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 | |
5 | import 'dart:convert'; |
6 | import 'dart:io' as io; |
7 | |
8 | import 'package:file/file.dart' ; |
9 | import 'package:file/local.dart' ; |
10 | import 'package:meta/meta.dart' ; |
11 | import 'package:platform/platform.dart' show LocalPlatform, Platform; |
12 | import 'package:process/process.dart' show LocalProcessManager, ProcessManager; |
13 | import 'package:pub_semver/pub_semver.dart' ; |
14 | |
15 | import 'data_types.dart'; |
16 | |
17 | /// An exception class to allow capture of exceptions generated by the Snippets |
18 | /// package. |
19 | class 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. |
38 | int getIndent(String line) => line.length - line.trimLeft().length; |
39 | |
40 | /// Contains information about the installed Flutter repo. |
41 | class 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. |
145 | String 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 |
|
184 | class 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 |
|
215 | Iterable<CodeSample> getSamplesInElements(Iterable<SourceElement>? elements) {
|
216 | return elements
|
217 | ?.expand<CodeSample>((SourceElement element) => element.samples) ??
|
218 | const <CodeSample>[];
|
219 | }
|
220 |
|
221 | SampleStats 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
|
268 | void Function(String message) errorExit = (String message) {
|
269 | io.stderr.writeln(message);
|
270 | io.exit(1);
|
271 | };
|
272 |
|