1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:convert';
6import 'dart:io' as io;
7
8import 'package:file/file.dart';
9import 'package:file/local.dart';
10import 'package:meta/meta.dart';
11import 'package:platform/platform.dart' show LocalPlatform, Platform;
12import 'package:process/process.dart' show LocalProcessManager, ProcessManager;
13import 'package:pub_semver/pub_semver.dart';
14
15import 'data_types.dart';
16
17/// An exception class to allow capture of exceptions generated by the Snippets
18/// package.
19class SnippetException implements Exception {
20 SnippetException(this.message, {this.file, this.line});
21 final String message;
22 final String? file;
23 final int? line;
24
25 @override
26 String toString() {
27 if (file != null || line != null) {
28 final String fileStr = file == null ? '' : '$file:';
29 final String lineStr = line == null ? '' : '$line:';
30 return '$runtimeType: $fileStr$lineStr: $message';
31 } else {
32 return '$runtimeType: $message';
33 }
34 }
35}
36
37/// Gets the number of whitespace characters at the beginning of a line.
38int getIndent(String line) => line.length - line.trimLeft().length;
39
40/// Contains information about the installed Flutter repo.
41class FlutterInformation {
42 FlutterInformation({
43 this.platform = const LocalPlatform(),
44 this.processManager = const LocalProcessManager(),
45 this.filesystem = const LocalFileSystem(),
46 });
47
48 final Platform platform;
49 final ProcessManager processManager;
50 final FileSystem filesystem;
51
52 static FlutterInformation? _instance;
53
54 static FlutterInformation get instance => _instance ??= FlutterInformation();
55
56 @visibleForTesting
57 static set instance(FlutterInformation? value) => _instance = value;
58
59 Directory getFlutterRoot() {
60 if (platform.environment['FLUTTER_ROOT'] != null) {
61 return filesystem.directory(platform.environment['FLUTTER_ROOT']);
62 }
63 return getFlutterInformation()['flutterRoot'] as Directory;
64 }
65
66 Version getFlutterVersion() =>
67 getFlutterInformation()['frameworkVersion'] as Version;
68
69 Version getDartSdkVersion() =>
70 getFlutterInformation()['dartSdkVersion'] as Version;
71
72 Map<String, dynamic>? _cachedFlutterInformation;
73
74 Map<String, dynamic> getFlutterInformation() {
75 if (_cachedFlutterInformation != null) {
76 return _cachedFlutterInformation!;
77 }
78
79 String flutterVersionJson;
80 if (platform.environment['FLUTTER_VERSION'] != null) {
81 flutterVersionJson = platform.environment['FLUTTER_VERSION']!;
82 } else {
83 String flutterCommand;
84 if (platform.environment['FLUTTER_ROOT'] != null) {
85 flutterCommand = filesystem
86 .directory(platform.environment['FLUTTER_ROOT'])
87 .childDirectory('bin')
88 .childFile('flutter')
89 .absolute
90 .path;
91 } else {
92 flutterCommand = 'flutter';
93 }
94 io.ProcessResult result;
95 try {
96 result = processManager.runSync(
97 <String>[flutterCommand, '--version', '--machine'],
98 stdoutEncoding: utf8);
99 } on io.ProcessException catch (e) {
100 throw SnippetException(
101 'Unable to determine Flutter information. Either set FLUTTER_ROOT, or place flutter command in your path.\n$e');
102 }
103 if (result.exitCode != 0) {
104 throw SnippetException(
105 'Unable to determine Flutter information, because of abnormal exit to flutter command.');
106 }
107 flutterVersionJson = (result.stdout as String).replaceAll(
108 'Waiting for another flutter command to release the startup lock...',
109 '');
110 }
111
112 final Map<String, dynamic> flutterVersion =
113 json.decode(flutterVersionJson) as Map<String, dynamic>;
114 if (flutterVersion['flutterRoot'] == null ||
115 flutterVersion['frameworkVersion'] == null ||
116 flutterVersion['dartSdkVersion'] == null) {
117 throw SnippetException(
118 'Flutter command output has unexpected format, unable to determine flutter root location.');
119 }
120
121 final Map<String, dynamic> info = <String, dynamic>{};
122 info['flutterRoot'] =
123 filesystem.directory(flutterVersion['flutterRoot']! as String);
124 info['frameworkVersion'] =
125 Version.parse(flutterVersion['frameworkVersion'] as String);
126
127 final RegExpMatch? dartVersionRegex =
128 RegExp(r'(?<base>[\d.]+)(?:\s+\(build (?<detail>[-.\w]+)\))?')
129 .firstMatch(flutterVersion['dartSdkVersion'] as String);
130 if (dartVersionRegex == null) {
131 throw SnippetException(
132 'Flutter command output has unexpected format, unable to parse dart SDK version ${flutterVersion['dartSdkVersion']}.');
133 }
134 info['dartSdkVersion'] = Version.parse(
135 dartVersionRegex.namedGroup('detail') ??
136 dartVersionRegex.namedGroup('base')!);
137 _cachedFlutterInformation = info;
138
139 return info;
140 }
141}
142
143/// Injects the [injections] into the [template], while turning the
144/// "description" injection into a comment.
145String interpolateTemplate(
146 List<SkeletonInjection> injections,
147 String template,
148 Map<String, Object?> metadata, {
149 bool addCopyright = false,
150}) {
151 String wrapSectionMarker(Iterable<String> contents, {required String name}) {
152 if (contents.join().trim().isEmpty) {
153 // Skip empty sections.
154 return '';
155 }
156 // We don't wrap some sections, because otherwise they generate invalid files.
157 final String result = <String>[
158 ...contents,
159 ].join('\n');
160 final RegExp wrappingNewlines = RegExp(r'^\n*(.*)\n*$', dotAll: true);
161 return result.replaceAllMapped(
162 wrappingNewlines, (Match match) => match.group(1)!);
163 }
164
165 return '${addCopyright ? '{{copyright}}\n\n' : ''}$template'
166 .replaceAllMapped(RegExp(r'{{([^}]+)}}'), (Match match) {
167 final String name = match[1]!;
168 final int componentIndex = injections
169 .indexWhere((SkeletonInjection injection) => injection.name == name);
170 if (metadata[name] != null && componentIndex == -1) {
171 // If the match isn't found in the injections, then just return the
172 // metadata entry.
173 return wrapSectionMarker((metadata[name]! as String).split('\n'),
174 name: name);
175 }
176 return wrapSectionMarker(
177 componentIndex >= 0
178 ? injections[componentIndex].stringContents
179 : <String>[],
180 name: name);
181 }).replaceAll(RegExp(r'\n\n+'), '\n\n');
182}
183
184class SampleStats {
185 const SampleStats({
186 this.totalSamples = 0,
187 this.dartpadSamples = 0,
188 this.snippetSamples = 0,
189 this.applicationSamples = 0,
190 this.wordCount = 0,
191 this.lineCount = 0,
192 this.linkCount = 0,
193 this.description = '',
194 });
195
196 final int totalSamples;
197 final int dartpadSamples;
198 final int snippetSamples;
199 final int applicationSamples;
200 final int wordCount;
201 final int lineCount;
202 final int linkCount;
203 final String description;
204 bool get allOneKind =>
205 totalSamples == snippetSamples ||
206 totalSamples == applicationSamples ||
207 totalSamples == dartpadSamples;
208
209 @override
210 String toString() {
211 return description;
212 }
213}
214
215Iterable<CodeSample> getSamplesInElements(Iterable<SourceElement>? elements) {
216 return elements
217 ?.expand<CodeSample>((SourceElement element) => element.samples) ??
218 const <CodeSample>[];
219}
220
221SampleStats getSampleStats(SourceElement element) {
222 if (element.comment.isEmpty) {
223 return const SampleStats();
224 }
225 final int total = element.sampleCount;
226 if (total == 0) {
227 return const SampleStats();
228 }
229 final int dartpads = element.dartpadSampleCount;
230 final int snippets = element.snippetCount;
231 final int applications = element.applicationSampleCount;
232 final String sampleCount = <String>[
233 if (snippets > 0) '$snippets snippet${snippets != 1 ? 's' : ''}',
234 if (applications > 0)
235 '$applications application sample${applications != 1 ? 's' : ''}',
236 if (dartpads > 0) '$dartpads dartpad sample${dartpads != 1 ? 's' : ''}'
237 ].join(', ');
238 final int wordCount = element.wordCount;
239 final int lineCount = element.lineCount;
240 final int linkCount = element.referenceCount;
241 final String description = <String>[
242 'Documentation has $wordCount ${wordCount == 1 ? 'word' : 'words'} on ',
243 '$lineCount ${lineCount == 1 ? 'line' : 'lines'}',
244 if (linkCount > 0 && element.hasSeeAlso) ', ',
245 if (linkCount > 0 && !element.hasSeeAlso) ' and ',
246 if (linkCount > 0)
247 'refers to $linkCount other ${linkCount == 1 ? 'symbol' : 'symbols'}',
248 if (linkCount > 0 && element.hasSeeAlso) ', and ',
249 if (linkCount == 0 && element.hasSeeAlso) 'and ',
250 if (element.hasSeeAlso) 'has a "See also:" section',
251 '.',
252 ].join();
253 return SampleStats(
254 totalSamples: total,
255 dartpadSamples: dartpads,
256 snippetSamples: snippets,
257 applicationSamples: applications,
258 wordCount: wordCount,
259 lineCount: lineCount,
260 linkCount: linkCount,
261 description: 'Has $sampleCount. $description',
262 );
263}
264
265/// Exit the app with a message to stderr.
266/// Can be overridden by tests to avoid exits.
267// ignore: prefer_function_declarations_over_variables
268void Function(String message) errorExit = (String message) {
269 io.stderr.writeln(message);
270 io.exit(1);
271};
272

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com