blob: e0cfdf4998682167438b1b95133b065964a043d7 [file] [log] [blame]
Ian Hickson449f4a62019-11-27 15:04:02 -08001// Copyright 2014 The Flutter Authors. All rights reserved.
Ian Hickson9ac16682017-06-12 16:52:35 -07002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Ian Hicksona516a242019-12-16 12:33:01 -08005// See ../snippets/README.md for documentation.
6
7// To run this, from the root of the Flutter repository:
8// bin/cache/dart-sdk/bin/dart dev/bots/analyze-sample-code.dart
Ian Hickson28352c32018-03-19 16:42:30 -07009
Ian Hickson9ac16682017-06-12 16:52:35 -070010import 'dart:io';
11
Greg Spencerbcd60fa2019-02-08 10:12:14 -080012import 'package:args/args.dart';
Ian Hickson9ac16682017-06-12 16:52:35 -070013import 'package:path/path.dart' as path;
14
15final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script))));
Greg Spencer202b0452018-11-05 07:31:35 -080016final String _defaultFlutterPackage = path.join(_flutterRoot, 'packages', 'flutter', 'lib');
Ian Hickson9ac16682017-06-12 16:52:35 -070017final String _flutter = path.join(_flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter');
18
Greg Spencer202b0452018-11-05 07:31:35 -080019void main(List<String> arguments) {
Greg Spencerbcd60fa2019-02-08 10:12:14 -080020 final ArgParser argParser = ArgParser();
21 argParser.addOption(
22 'temp',
23 defaultsTo: null,
24 help: 'A location where temporary files may be written. Defaults to a '
Ian Hicksona516a242019-12-16 12:33:01 -080025 'directory in the system temp folder. If specified, will not be '
26 'automatically removed at the end of execution.',
Greg Spencerbcd60fa2019-02-08 10:12:14 -080027 );
28 argParser.addFlag(
Greg Spencera3186fb2019-11-19 15:16:12 -080029 'verbose',
30 defaultsTo: false,
31 negatable: false,
32 help: 'Print verbose output for the analysis process.',
33 );
34 argParser.addFlag(
Greg Spencerbcd60fa2019-02-08 10:12:14 -080035 'help',
36 defaultsTo: false,
37 negatable: false,
38 help: 'Print help for this command.',
39 );
40
41 final ArgResults parsedArguments = argParser.parse(arguments);
42
Alexandre Ardhuinec1a0152019-12-05 22:34:06 +010043 if (parsedArguments['help'] as bool) {
Greg Spencerbcd60fa2019-02-08 10:12:14 -080044 print(argParser.usage);
Ian Hicksona516a242019-12-16 12:33:01 -080045 print('See dev/snippets/README.md for documentation.');
Greg Spencerbcd60fa2019-02-08 10:12:14 -080046 exit(0);
47 }
48
Greg Spencer202b0452018-11-05 07:31:35 -080049 Directory flutterPackage;
Greg Spencerbcd60fa2019-02-08 10:12:14 -080050 if (parsedArguments.rest.length == 1) {
Greg Spencer202b0452018-11-05 07:31:35 -080051 // Used for testing.
Greg Spencerbcd60fa2019-02-08 10:12:14 -080052 flutterPackage = Directory(parsedArguments.rest.single);
Greg Spencer202b0452018-11-05 07:31:35 -080053 } else {
54 flutterPackage = Directory(_defaultFlutterPackage);
Ian Hickson9ac16682017-06-12 16:52:35 -070055 }
Greg Spencerbcd60fa2019-02-08 10:12:14 -080056
57 Directory tempDirectory;
58 if (parsedArguments.wasParsed('temp')) {
Alexandre Ardhuinec1a0152019-12-05 22:34:06 +010059 final String tempArg = parsedArguments['temp'] as String;
60 tempDirectory = Directory(path.join(Directory.systemTemp.absolute.path, path.basename(tempArg)));
61 if (path.basename(tempArg) != tempArg) {
Greg Spencer262f12b2019-02-15 07:48:49 -080062 stderr.writeln('Supplied temporary directory name should be a name, not a path. Using ${tempDirectory.absolute.path} instead.');
63 }
64 print('Leaving temporary output in ${tempDirectory.absolute.path}.');
65 // Make sure that any directory left around from a previous run is cleared
66 // out.
67 if (tempDirectory.existsSync()) {
68 tempDirectory.deleteSync(recursive: true);
69 }
70 tempDirectory.createSync();
71 }
72 try {
Alexandre Ardhuinec1a0152019-12-05 22:34:06 +010073 exitCode = SampleChecker(
74 flutterPackage,
75 tempDirectory: tempDirectory,
76 verbose: parsedArguments['verbose'] as bool,
77 ).checkSamples();
Greg Spencer262f12b2019-02-15 07:48:49 -080078 } on SampleCheckerException catch (e) {
79 stderr.write(e);
80 exit(1);
81 }
82}
83
84class SampleCheckerException implements Exception {
85 SampleCheckerException(this.message, {this.file, this.line});
86 final String message;
87 final String file;
88 final int line;
89
90 @override
91 String toString() {
92 if (file != null || line != null) {
93 final String fileStr = file == null ? '' : '$file:';
94 final String lineStr = line == null ? '' : '$line:';
95 return '$fileStr$lineStr Error: $message';
96 } else {
97 return 'Error: $message';
Greg Spencerbcd60fa2019-02-08 10:12:14 -080098 }
99 }
Ian Hickson9ac16682017-06-12 16:52:35 -0700100}
101
Greg Spencer202b0452018-11-05 07:31:35 -0800102/// Checks samples and code snippets for analysis errors.
103///
104/// Extracts dartdoc content from flutter package source code, identifies code
105/// sections, and writes them to a temporary directory, where 'flutter analyze'
106/// is used to analyze the sources for problems. If problems are found, the
107/// error output from the analyzer is parsed for details, and the problem
108/// locations are translated back to the source location.
109///
Greg Spencerfabf4e32020-01-08 15:28:02 -0800110/// For samples, the samples are generated using the snippets tool, and they
111/// are analyzed with the snippets. If errors are found in samples, then the
112/// line number of the start of the sample is given instead of the actual error
113/// line, since samples get reformatted when written, and the line numbers
Greg Spencer202b0452018-11-05 07:31:35 -0800114/// don't necessarily match. It does, however, print the source of the
115/// problematic line.
116class SampleChecker {
Greg Spencera3186fb2019-11-19 15:16:12 -0800117 SampleChecker(this._flutterPackage, {Directory tempDirectory, this.verbose = false})
Greg Spencerbcd60fa2019-02-08 10:12:14 -0800118 : _tempDirectory = tempDirectory,
119 _keepTmp = tempDirectory != null {
120 _tempDirectory ??= Directory.systemTemp.createTempSync('flutter_analyze_sample_code.');
Ian Hickson9ac16682017-06-12 16:52:35 -0700121 }
Greg Spencer202b0452018-11-05 07:31:35 -0800122
123 /// The prefix of each comment line
124 static const String _dartDocPrefix = '///';
125
126 /// The prefix of each comment line with a space appended.
127 static const String _dartDocPrefixWithSpace = '$_dartDocPrefix ';
128
129 /// A RegExp that matches the beginning of a dartdoc snippet or sample.
Greg Spencera3186fb2019-11-19 15:16:12 -0800130 static final RegExp _dartDocSampleBeginRegex = RegExp(r'{@tool (sample|snippet|dartpad)(?:| ([^}]*))}');
Greg Spencer202b0452018-11-05 07:31:35 -0800131
132 /// A RegExp that matches the end of a dartdoc snippet or sample.
133 static final RegExp _dartDocSampleEndRegex = RegExp(r'{@end-tool}');
134
135 /// A RegExp that matches the start of a code block within dartdoc.
Greg Spencer262f12b2019-02-15 07:48:49 -0800136 static final RegExp _codeBlockStartRegex = RegExp(r'///\s+```dart.*$');
Greg Spencer202b0452018-11-05 07:31:35 -0800137
138 /// A RegExp that matches the end of a code block within dartdoc.
Greg Spencer262f12b2019-02-15 07:48:49 -0800139 static final RegExp _codeBlockEndRegex = RegExp(r'///\s+```\s*$');
Greg Spencer202b0452018-11-05 07:31:35 -0800140
141 /// A RegExp that matches a Dart constructor.
LongCatIsLooong712195b2019-04-01 17:27:29 -0700142 static final RegExp _constructorRegExp = RegExp(r'(const\s+)?_*[A-Z][a-zA-Z0-9<>._]*\(');
Greg Spencer202b0452018-11-05 07:31:35 -0800143
Greg Spencera3186fb2019-11-19 15:16:12 -0800144 /// Whether or not to print verbose output.
145 final bool verbose;
146
Greg Spencerbcd60fa2019-02-08 10:12:14 -0800147 /// Whether or not to keep the temp directory around after running.
148 ///
149 /// Defaults to false.
150 final bool _keepTmp;
151
Greg Spencer202b0452018-11-05 07:31:35 -0800152 /// The temporary directory where all output is written. This will be deleted
153 /// automatically if there are no errors.
Greg Spencerbcd60fa2019-02-08 10:12:14 -0800154 Directory _tempDirectory;
Greg Spencer202b0452018-11-05 07:31:35 -0800155
156 /// The package directory for the flutter package within the flutter root dir.
157 final Directory _flutterPackage;
158
159 /// A serial number so that we can create unique expression names when we
160 /// generate them.
161 int _expressionId = 0;
162
163 /// The exit code from the analysis process.
164 int _exitCode = 0;
165
166 // Once the snippets tool has been precompiled by Dart, this contains the AOT
167 // snapshot.
168 String _snippetsSnapshotPath;
169
170 /// Finds the location of the snippets script.
171 String get _snippetsExecutable {
Janice Collins4a110b62018-12-12 09:40:18 -0800172 final String platformScriptPath = path.dirname(path.fromUri(Platform.script));
Greg Spencer202b0452018-11-05 07:31:35 -0800173 return path.canonicalize(path.join(platformScriptPath, '..', 'snippets', 'lib', 'main.dart'));
Ian Hickson9ac16682017-06-12 16:52:35 -0700174 }
Ian Hickson9ac16682017-06-12 16:52:35 -0700175
Greg Spencerbcd60fa2019-02-08 10:12:14 -0800176 /// Finds the location of the Dart executable.
177 String get _dartExecutable {
178 final File dartExecutable = File(Platform.resolvedExecutable);
179 return dartExecutable.absolute.path;
180 }
181
Greg Spencer202b0452018-11-05 07:31:35 -0800182 static List<File> _listDartFiles(Directory directory, {bool recursive = false}) {
Greg Spencer262f12b2019-02-15 07:48:49 -0800183 return directory.listSync(recursive: recursive, followLinks: false).whereType<File>().where((File file) => path.extension(file.path) == '.dart').toList();
Greg Spencer202b0452018-11-05 07:31:35 -0800184 }
Ian Hickson9ac16682017-06-12 16:52:35 -0700185
Greg Spencer202b0452018-11-05 07:31:35 -0800186 /// Computes the headers needed for each sample file.
187 List<Line> get headers {
Alexandre Ardhuindf4bf452019-09-17 16:23:44 +0200188 return _headers ??= <String>[
189 '// generated code',
190 "import 'dart:async';",
191 "import 'dart:convert';",
192 "import 'dart:math' as math;",
193 "import 'dart:typed_data';",
194 "import 'dart:ui' as ui;",
195 "import 'package:flutter_test/flutter_test.dart';",
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100196 for (final File file in _listDartFiles(Directory(_defaultFlutterPackage))) ...<String>[
Alexandre Ardhuindf4bf452019-09-17 16:23:44 +0200197 '',
198 '// ${file.path}',
199 "import 'package:flutter/${path.basename(file.path)}';",
200 ],
201 ].map<Line>((String code) => Line(code)).toList();
Greg Spencer202b0452018-11-05 07:31:35 -0800202 }
203
204 List<Line> _headers;
205
206 /// Checks all the samples in the Dart files in [_flutterPackage] for errors.
207 int checkSamples() {
208 _exitCode = 0;
209 Map<String, List<AnalysisError>> errors = <String, List<AnalysisError>>{};
210 try {
211 final Map<String, Section> sections = <String, Section>{};
Greg Spencerfabf4e32020-01-08 15:28:02 -0800212 final Map<String, Sample> snippets = <String, Sample>{};
Greg Spencer202b0452018-11-05 07:31:35 -0800213 _extractSamples(sections, snippets);
Greg Spencerbcd60fa2019-02-08 10:12:14 -0800214 errors = _analyze(_tempDirectory, sections, snippets);
Greg Spencer202b0452018-11-05 07:31:35 -0800215 } finally {
216 if (errors.isNotEmpty) {
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100217 for (final String filePath in errors.keys) {
Greg Spencer202b0452018-11-05 07:31:35 -0800218 errors[filePath].forEach(stderr.writeln);
219 }
220 stderr.writeln('\nFound ${errors.length} sample code errors.');
221 }
Greg Spencer262f12b2019-02-15 07:48:49 -0800222 if (_keepTmp) {
223 print('Leaving temporary directory ${_tempDirectory.path} around for your perusal.');
224 } else {
Greg Spencerbcd60fa2019-02-08 10:12:14 -0800225 try {
226 _tempDirectory.deleteSync(recursive: true);
227 } on FileSystemException catch (e) {
228 stderr.writeln('Failed to delete ${_tempDirectory.path}: $e');
229 }
Greg Spencer202b0452018-11-05 07:31:35 -0800230 }
231 // If we made a snapshot, remove it (so as not to clutter up the tree).
232 if (_snippetsSnapshotPath != null) {
233 final File snapshot = File(_snippetsSnapshotPath);
234 if (snapshot.existsSync()) {
235 snapshot.deleteSync();
236 }
237 }
238 }
239 return _exitCode;
240 }
241
242 /// Creates a name for the snippets tool to use for the snippet ID from a
243 /// filename and starting line number.
244 String _createNameFromSource(String prefix, String filename, int start) {
Greg Spencerfabf4e32020-01-08 15:28:02 -0800245 String sampleId = path.split(filename).join('.');
246 sampleId = path.basenameWithoutExtension(sampleId);
247 sampleId = '$prefix.$sampleId.$start';
248 return sampleId;
Greg Spencer202b0452018-11-05 07:31:35 -0800249 }
250
251 // Precompiles the snippets tool if _snippetsSnapshotPath isn't set yet, and
252 // runs the precompiled version if it is set.
253 ProcessResult _runSnippetsScript(List<String> args) {
Greg Spencer094f93d2018-11-07 08:29:14 -0800254 final String workingDirectory = path.join(_flutterRoot, 'dev', 'docs');
Greg Spencer202b0452018-11-05 07:31:35 -0800255 if (_snippetsSnapshotPath == null) {
256 _snippetsSnapshotPath = '$_snippetsExecutable.snapshot';
257 return Process.runSync(
Greg Spencerbcd60fa2019-02-08 10:12:14 -0800258 _dartExecutable,
Greg Spencer202b0452018-11-05 07:31:35 -0800259 <String>[
260 '--snapshot=$_snippetsSnapshotPath',
261 '--snapshot-kind=app-jit',
Janice Collins4a110b62018-12-12 09:40:18 -0800262 path.canonicalize(_snippetsExecutable),
Alexandre Ardhuin919dcf52019-06-27 21:23:16 +0200263 ...args,
264 ],
Greg Spencer094f93d2018-11-07 08:29:14 -0800265 workingDirectory: workingDirectory,
Greg Spencer202b0452018-11-05 07:31:35 -0800266 );
267 } else {
268 return Process.runSync(
Greg Spencerbcd60fa2019-02-08 10:12:14 -0800269 _dartExecutable,
Alexandre Ardhuin919dcf52019-06-27 21:23:16 +0200270 <String>[path.canonicalize(_snippetsSnapshotPath), ...args],
Greg Spencer094f93d2018-11-07 08:29:14 -0800271 workingDirectory: workingDirectory,
Greg Spencer202b0452018-11-05 07:31:35 -0800272 );
273 }
274 }
275
Greg Spencerfabf4e32020-01-08 15:28:02 -0800276 /// Writes out the given sample to an output file in the [_tempDirectory] and
Greg Spencer202b0452018-11-05 07:31:35 -0800277 /// returns the output file.
Greg Spencerfabf4e32020-01-08 15:28:02 -0800278 File _writeSample(Sample sample) {
Greg Spencer202b0452018-11-05 07:31:35 -0800279 // Generate the snippet.
Greg Spencerfabf4e32020-01-08 15:28:02 -0800280 final String sampleId = _createNameFromSource('sample', sample.start.filename, sample.start.line);
281 final String inputName = '$sampleId.input';
Greg Spencer202b0452018-11-05 07:31:35 -0800282 // Now we have a filename like 'lib.src.material.foo_widget.123.dart' for each snippet.
Greg Spencerbcd60fa2019-02-08 10:12:14 -0800283 final File inputFile = File(path.join(_tempDirectory.path, inputName))..createSync(recursive: true);
Greg Spencerfabf4e32020-01-08 15:28:02 -0800284 inputFile.writeAsStringSync(sample.input.join('\n'));
285 final File outputFile = File(path.join(_tempDirectory.path, '$sampleId.dart'));
Greg Spencer202b0452018-11-05 07:31:35 -0800286 final List<String> args = <String>[
287 '--output=${outputFile.absolute.path}',
288 '--input=${inputFile.absolute.path}',
Greg Spencerfabf4e32020-01-08 15:28:02 -0800289 ...sample.args,
Alexandre Ardhuin919dcf52019-06-27 21:23:16 +0200290 ];
Ian Hicksona516a242019-12-16 12:33:01 -0800291 if (verbose)
Greg Spencerfabf4e32020-01-08 15:28:02 -0800292 print('Generating sample for ${sample.start?.filename}:${sample.start?.line}');
Greg Spencer202b0452018-11-05 07:31:35 -0800293 final ProcessResult process = _runSnippetsScript(args);
Ian Hicksona516a242019-12-16 12:33:01 -0800294 if (verbose)
Greg Spencera3186fb2019-11-19 15:16:12 -0800295 stderr.write('${process.stderr}');
Greg Spencer202b0452018-11-05 07:31:35 -0800296 if (process.exitCode != 0) {
Greg Spencer262f12b2019-02-15 07:48:49 -0800297 throw SampleCheckerException(
Greg Spencerfabf4e32020-01-08 15:28:02 -0800298 'Unable to create sample for ${sample.start.filename}:${sample.start.line} '
Greg Spencer262f12b2019-02-15 07:48:49 -0800299 '(using input from ${inputFile.path}):\n${process.stdout}\n${process.stderr}',
Greg Spencerfabf4e32020-01-08 15:28:02 -0800300 file: sample.start.filename,
301 line: sample.start.line,
Greg Spencer262f12b2019-02-15 07:48:49 -0800302 );
Greg Spencer202b0452018-11-05 07:31:35 -0800303 }
304 return outputFile;
305 }
306
307 /// Extracts the samples from the Dart files in [_flutterPackage], writes them
Greg Spencerfabf4e32020-01-08 15:28:02 -0800308 /// to disk, and adds them to the appropriate [sectionMap] or [sampleMap].
309 void _extractSamples(Map<String, Section> sectionMap, Map<String, Sample> sampleMap) {
Greg Spencer202b0452018-11-05 07:31:35 -0800310 final List<Section> sections = <Section>[];
Greg Spencerfabf4e32020-01-08 15:28:02 -0800311 final List<Sample> samples = <Sample>[];
Greg Spencer202b0452018-11-05 07:31:35 -0800312
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100313 for (final File file in _listDartFiles(_flutterPackage, recursive: true)) {
Greg Spencer202b0452018-11-05 07:31:35 -0800314 final String relativeFilePath = path.relative(file.path, from: _flutterPackage.path);
315 final List<String> sampleLines = file.readAsLinesSync();
316 final List<Section> preambleSections = <Section>[];
Greg Spencer262f12b2019-02-15 07:48:49 -0800317 // Whether or not we're in the file-wide preamble section ("Examples can assume").
Greg Spencer202b0452018-11-05 07:31:35 -0800318 bool inPreamble = false;
Greg Spencer262f12b2019-02-15 07:48:49 -0800319 // Whether or not we're in a code sample
Greg Spencer202b0452018-11-05 07:31:35 -0800320 bool inSampleSection = false;
Greg Spencer262f12b2019-02-15 07:48:49 -0800321 // Whether or not we're in a snippet code sample (with template) specifically.
Greg Spencer202b0452018-11-05 07:31:35 -0800322 bool inSnippet = false;
Greg Spencer262f12b2019-02-15 07:48:49 -0800323 // Whether or not we're in a '```dart' segment.
Greg Spencer202b0452018-11-05 07:31:35 -0800324 bool inDart = false;
Greg Spencer202b0452018-11-05 07:31:35 -0800325 int lineNumber = 0;
326 final List<String> block = <String>[];
327 List<String> snippetArgs = <String>[];
328 Line startLine;
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100329 for (final String line in sampleLines) {
Greg Spencer202b0452018-11-05 07:31:35 -0800330 lineNumber += 1;
331 final String trimmedLine = line.trim();
332 if (inSnippet) {
333 if (!trimmedLine.startsWith(_dartDocPrefix)) {
Greg Spencer262f12b2019-02-15 07:48:49 -0800334 throw SampleCheckerException('Snippet section unterminated.', file: relativeFilePath, line: lineNumber);
Greg Spencer202b0452018-11-05 07:31:35 -0800335 }
336 if (_dartDocSampleEndRegex.hasMatch(trimmedLine)) {
Greg Spencerfabf4e32020-01-08 15:28:02 -0800337 samples.add(
338 Sample(
Greg Spencer202b0452018-11-05 07:31:35 -0800339 start: startLine,
340 input: block,
341 args: snippetArgs,
Greg Spencerfabf4e32020-01-08 15:28:02 -0800342 serial: samples.length,
Greg Spencer202b0452018-11-05 07:31:35 -0800343 ),
344 );
345 snippetArgs = <String>[];
346 block.clear();
347 inSnippet = false;
348 inSampleSection = false;
349 } else {
350 block.add(line.replaceFirst(RegExp(r'\s*/// ?'), ''));
351 }
352 } else if (inPreamble) {
353 if (line.isEmpty) {
354 inPreamble = false;
355 preambleSections.add(_processBlock(startLine, block));
356 block.clear();
357 } else if (!line.startsWith('// ')) {
Greg Spencer262f12b2019-02-15 07:48:49 -0800358 throw SampleCheckerException('Unexpected content in sample code preamble.', file: relativeFilePath, line: lineNumber);
Greg Spencer202b0452018-11-05 07:31:35 -0800359 } else {
360 block.add(line.substring(3));
361 }
362 } else if (inSampleSection) {
Greg Spencer262f12b2019-02-15 07:48:49 -0800363 if (_dartDocSampleEndRegex.hasMatch(trimmedLine)) {
Greg Spencer202b0452018-11-05 07:31:35 -0800364 if (inDart) {
Greg Spencer262f12b2019-02-15 07:48:49 -0800365 throw SampleCheckerException("Dart section didn't terminate before end of sample", file: relativeFilePath, line: lineNumber);
Greg Spencer202b0452018-11-05 07:31:35 -0800366 }
367 inSampleSection = false;
Greg Spencer262f12b2019-02-15 07:48:49 -0800368 }
369 if (inDart) {
370 if (_codeBlockEndRegex.hasMatch(trimmedLine)) {
371 inDart = false;
372 final Section processed = _processBlock(startLine, block);
373 if (preambleSections.isEmpty) {
374 sections.add(processed);
Greg Spencer202b0452018-11-05 07:31:35 -0800375 } else {
Alexandre Ardhuin9c31f9f2019-07-01 07:05:42 +0200376 sections.add(Section.combine(preambleSections..add(processed)));
Greg Spencer202b0452018-11-05 07:31:35 -0800377 }
Greg Spencer262f12b2019-02-15 07:48:49 -0800378 block.clear();
379 } else if (trimmedLine == _dartDocPrefix) {
380 block.add('');
381 } else {
382 final int index = line.indexOf(_dartDocPrefixWithSpace);
383 if (index < 0) {
384 throw SampleCheckerException(
385 'Dart section inexplicably did not contain "$_dartDocPrefixWithSpace" prefix.',
386 file: relativeFilePath,
387 line: lineNumber,
388 );
389 }
390 block.add(line.substring(index + 4));
Greg Spencer202b0452018-11-05 07:31:35 -0800391 }
Greg Spencer262f12b2019-02-15 07:48:49 -0800392 } else if (_codeBlockStartRegex.hasMatch(trimmedLine)) {
393 assert(block.isEmpty);
394 startLine = Line(
395 '',
396 filename: relativeFilePath,
397 line: lineNumber + 1,
398 indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length,
399 );
400 inDart = true;
Greg Spencer202b0452018-11-05 07:31:35 -0800401 }
402 }
403 if (!inSampleSection) {
404 final Match sampleMatch = _dartDocSampleBeginRegex.firstMatch(trimmedLine);
405 if (line == '// Examples can assume:') {
406 assert(block.isEmpty);
407 startLine = Line('', filename: relativeFilePath, line: lineNumber + 1, indent: 3);
408 inPreamble = true;
Greg Spencer262f12b2019-02-15 07:48:49 -0800409 } else if (sampleMatch != null) {
Greg Spencerfabf4e32020-01-08 15:28:02 -0800410 inSnippet = sampleMatch != null && (sampleMatch[1] == 'sample' || sampleMatch[1] == 'dartpad');
Greg Spencer202b0452018-11-05 07:31:35 -0800411 if (inSnippet) {
412 startLine = Line(
413 '',
414 filename: relativeFilePath,
415 line: lineNumber + 1,
416 indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length,
417 );
418 if (sampleMatch[2] != null) {
419 // There are arguments to the snippet tool to keep track of.
420 snippetArgs = _splitUpQuotedArgs(sampleMatch[2]).toList();
421 } else {
422 snippetArgs = <String>[];
423 }
424 }
425 inSampleSection = !inSnippet;
Greg Spencer262f12b2019-02-15 07:48:49 -0800426 } else if (RegExp(r'///\s*#+\s+[Ss]ample\s+[Cc]ode:?$').hasMatch(trimmedLine)) {
427 throw SampleCheckerException(
Greg Spencerfabf4e32020-01-08 15:28:02 -0800428 "Found deprecated '## Sample code' section: use {@tool snippet}...{@end-tool} instead.",
Greg Spencer262f12b2019-02-15 07:48:49 -0800429 file: relativeFilePath,
430 line: lineNumber,
431 );
Greg Spencer202b0452018-11-05 07:31:35 -0800432 }
433 }
434 }
435 }
436 print('Found ${sections.length} sample code sections.');
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100437 for (final Section section in sections) {
Greg Spencer202b0452018-11-05 07:31:35 -0800438 sectionMap[_writeSection(section).path] = section;
Ian Hickson9ac16682017-06-12 16:52:35 -0700439 }
Greg Spencerfabf4e32020-01-08 15:28:02 -0800440 for (final Sample sample in samples) {
441 final File snippetFile = _writeSample(sample);
442 sample.contents = snippetFile.readAsLinesSync();
443 sampleMap[snippetFile.absolute.path] = sample;
Greg Spencer202b0452018-11-05 07:31:35 -0800444 }
445 }
446
447 /// Helper to process arguments given as a (possibly quoted) string.
448 ///
449 /// First, this will split the given [argsAsString] into separate arguments,
450 /// taking any quoting (either ' or " are accepted) into account, including
451 /// handling backslash-escaped quotes.
452 ///
453 /// Then, it will prepend "--" to any args that start with an identifier
454 /// followed by an equals sign, allowing the argument parser to treat any
455 /// "foo=bar" argument as "--foo=bar" (which is a dartdoc-ism).
456 Iterable<String> _splitUpQuotedArgs(String argsAsString) {
457 // Regexp to take care of splitting arguments, and handling the quotes
458 // around arguments, if any.
459 //
460 // Match group 1 is the "foo=" (or "--foo=") part of the option, if any.
461 // Match group 2 contains the quote character used (which is discarded).
462 // Match group 3 is a quoted arg, if any, without the quotes.
463 // Match group 4 is the unquoted arg, if any.
464 final RegExp argMatcher = RegExp(r'([a-zA-Z\-_0-9]+=)?' // option name
465 r'(?:' // Start a new non-capture group for the two possibilities.
466 r'''(["'])((?:\\{2})*|(?:.*?[^\\](?:\\{2})*))\2|''' // with quotes.
467 r'([^ ]+))'); // without quotes.
468 final Iterable<Match> matches = argMatcher.allMatches(argsAsString);
469
470 // Remove quotes around args, and if convertToArgs is true, then for any
471 // args that look like assignments (start with valid option names followed
472 // by an equals sign), add a "--" in front so that they parse as options.
473 return matches.map<String>((Match match) {
474 String option = '';
475 if (match[1] != null && !match[1].startsWith('-')) {
476 option = '--';
477 }
478 if (match[2] != null) {
479 // This arg has quotes, so strip them.
480 return '$option${match[1] ?? ''}${match[3] ?? ''}${match[4] ?? ''}';
481 }
482 return '$option${match[0]}';
483 });
484 }
485
486 /// Creates the configuration files necessary for the analyzer to consider
487 /// the temporary director a package, and sets which lint rules to enforce.
488 void _createConfigurationFiles(Directory directory) {
489 final File pubSpec = File(path.join(directory.path, 'pubspec.yaml'))..createSync(recursive: true);
490 final File analysisOptions = File(path.join(directory.path, 'analysis_options.yaml'))..createSync(recursive: true);
Ian Hickson9ac16682017-06-12 16:52:35 -0700491 pubSpec.writeAsStringSync('''
492name: analyze_sample_code
493dependencies:
494 flutter:
495 sdk: flutter
Ian Hicksonded39052018-03-11 03:19:18 -0700496 flutter_test:
497 sdk: flutter
Ian Hickson9ac16682017-06-12 16:52:35 -0700498''');
Alexandre Ardhuin3c581952018-09-07 09:13:13 +0200499 analysisOptions.writeAsStringSync('''
500linter:
501 rules:
502 - unnecessary_const
Alexandre Ardhuin976cffa2018-09-07 11:00:05 +0200503 - unnecessary_new
Alexandre Ardhuin3c581952018-09-07 09:13:13 +0200504''');
Greg Spencer202b0452018-11-05 07:31:35 -0800505 }
506
507 /// Writes out a sample section to the disk and returns the file.
508 File _writeSection(Section section) {
Greg Spencerfabf4e32020-01-08 15:28:02 -0800509 final String sectionId = _createNameFromSource('snippet', section.start.filename, section.start.line);
Greg Spencerbcd60fa2019-02-08 10:12:14 -0800510 final File outputFile = File(path.join(_tempDirectory.path, '$sectionId.dart'))..createSync(recursive: true);
Alexandre Ardhuin758009b2019-07-02 21:11:56 +0200511 final List<Line> mainContents = <Line>[
512 ...headers,
513 const Line(''),
514 Line('// From: ${section.start.filename}:${section.start.line}'),
515 ...section.code,
516 ];
Greg Spencer202b0452018-11-05 07:31:35 -0800517 outputFile.writeAsStringSync(mainContents.map<String>((Line line) => line.code).join('\n'));
518 return outputFile;
519 }
520
521 /// Invokes the analyzer on the given [directory] and returns the stdout.
522 List<String> _runAnalyzer(Directory directory) {
Greg Spencerfabf4e32020-01-08 15:28:02 -0800523 print('Starting analysis of code samples.');
Greg Spencer202b0452018-11-05 07:31:35 -0800524 _createConfigurationFiles(directory);
525 final ProcessResult result = Process.runSync(
Ian Hickson9ac16682017-06-12 16:52:35 -0700526 _flutter,
Greg Spencer202b0452018-11-05 07:31:35 -0800527 <String>['--no-wrap', 'analyze', '--no-preamble', '--no-congratulate', '.'],
528 workingDirectory: directory.absolute.path,
Ian Hickson9ac16682017-06-12 16:52:35 -0700529 );
Greg Spencer202b0452018-11-05 07:31:35 -0800530 final List<String> stderr = result.stderr.toString().trim().split('\n');
531 final List<String> stdout = result.stdout.toString().trim().split('\n');
532 // Check out the stderr to see if the analyzer had it's own issues.
533 if (stderr.isNotEmpty && (stderr.first.contains(' issues found. (ran in ') || stderr.first.contains(' issue found. (ran in '))) {
534 // The "23 issues found" message goes onto stderr, which is concatenated first.
535 stderr.removeAt(0);
536 // If there's an "issues found" message, we put a blank line on stdout before it.
537 if (stderr.isNotEmpty && stderr.last.isEmpty) {
538 stderr.removeLast();
539 }
Ian Hicksonad1eaff2018-08-20 12:51:07 -0700540 }
Greg Spencer202b0452018-11-05 07:31:35 -0800541 if (stderr.isNotEmpty) {
542 throw 'Cannot analyze dartdocs; unexpected error output:\n$stderr';
543 }
544 if (stdout.isNotEmpty && stdout.first == 'Building flutter tool...') {
545 stdout.removeAt(0);
546 }
Michael Thomsen7ae3caf2019-05-21 16:38:58 +0200547 if (stdout.isNotEmpty && stdout.first.startsWith('Running "flutter pub get" in ')) {
Greg Spencer202b0452018-11-05 07:31:35 -0800548 stdout.removeAt(0);
549 }
550 _exitCode = result.exitCode;
551 return stdout;
552 }
553
554 /// Starts the analysis phase of checking the samples by invoking the analyzer
555 /// and parsing its output to create a map of filename to [AnalysisError]s.
556 Map<String, List<AnalysisError>> _analyze(
557 Directory directory,
558 Map<String, Section> sections,
Greg Spencerfabf4e32020-01-08 15:28:02 -0800559 Map<String, Sample> samples,
Greg Spencer202b0452018-11-05 07:31:35 -0800560 ) {
561 final List<String> errors = _runAnalyzer(directory);
562 final Map<String, List<AnalysisError>> analysisErrors = <String, List<AnalysisError>>{};
563 void addAnalysisError(File file, AnalysisError error) {
564 if (analysisErrors.containsKey(file.path)) {
565 analysisErrors[file.path].add(error);
566 } else {
567 analysisErrors[file.path] = <AnalysisError>[error];
568 }
569 }
570
Ian Hicksonad1eaff2018-08-20 12:51:07 -0700571 final String kBullet = Platform.isWindows ? ' - ' : ' • ';
Greg Spencer202b0452018-11-05 07:31:35 -0800572 // RegExp to match an error output line of the analyzer.
573 final RegExp errorPattern = RegExp(
574 '^ +([a-z]+)$kBullet(.+)$kBullet(.+):([0-9]+):([0-9]+)$kBullet([-a-z_]+)\$',
575 caseSensitive: false,
576 );
577 bool unknownAnalyzerErrors = false;
578 final int headerLength = headers.length + 2;
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100579 for (final String error in errors) {
Ian Hicksonad1eaff2018-08-20 12:51:07 -0700580 final Match parts = errorPattern.matchAsPrefix(error);
581 if (parts != null) {
582 final String message = parts[2];
Greg Spencerbcd60fa2019-02-08 10:12:14 -0800583 final File file = File(path.join(_tempDirectory.path, parts[3]));
Greg Spencer202b0452018-11-05 07:31:35 -0800584 final List<String> fileContents = file.readAsLinesSync();
585 final bool isSnippet = path.basename(file.path).startsWith('snippet.');
586 final bool isSample = path.basename(file.path).startsWith('sample.');
Ian Hicksonad1eaff2018-08-20 12:51:07 -0700587 final String line = parts[4];
588 final String column = parts[5];
589 final String errorCode = parts[6];
Greg Spencerfabf4e32020-01-08 15:28:02 -0800590 final int lineNumber = int.parse(line, radix: 10) - (isSnippet ? headerLength : 0);
Ian Hicksonad1eaff2018-08-20 12:51:07 -0700591 final int columnNumber = int.parse(column, radix: 10);
Greg Spencer202b0452018-11-05 07:31:35 -0800592 if (lineNumber < 0 && errorCode == 'unused_import') {
593 // We don't care about unused imports.
594 continue;
Alexander Apreleve0cd42e2018-04-12 16:28:01 -0700595 }
Greg Spencer202b0452018-11-05 07:31:35 -0800596
597 // For when errors occur outside of the things we're trying to analyze.
598 if (!isSnippet && !isSample) {
599 addAnalysisError(
600 file,
601 AnalysisError(
602 lineNumber,
603 columnNumber,
604 message,
605 errorCode,
606 Line(
607 '',
608 filename: file.path,
609 line: lineNumber,
610 ),
611 ),
612 );
Greg Spencer262f12b2019-02-15 07:48:49 -0800613 throw SampleCheckerException(
614 'Cannot analyze dartdocs; analysis errors exist: $error',
615 file: file.path,
616 line: lineNumber,
617 );
Ian Hicksona29d7232018-01-20 01:42:55 -0800618 }
Greg Spencer202b0452018-11-05 07:31:35 -0800619
Ian Hickson6d134e02018-09-23 00:43:05 -0700620 if (errorCode == 'unused_element' || errorCode == 'unused_local_variable') {
Ian Hickson9ac16682017-06-12 16:52:35 -0700621 // We don't really care if sample code isn't used!
Greg Spencer202b0452018-11-05 07:31:35 -0800622 continue;
623 }
Greg Spencerfabf4e32020-01-08 15:28:02 -0800624 if (isSample) {
Greg Spencer202b0452018-11-05 07:31:35 -0800625 addAnalysisError(
626 file,
627 AnalysisError(
628 lineNumber,
629 columnNumber,
630 message,
631 errorCode,
632 null,
Greg Spencerfabf4e32020-01-08 15:28:02 -0800633 sample: samples[file.path],
Greg Spencer202b0452018-11-05 07:31:35 -0800634 ),
635 );
Ian Hickson9ac16682017-06-12 16:52:35 -0700636 } else {
Greg Spencer202b0452018-11-05 07:31:35 -0800637 if (lineNumber < 1 || lineNumber > fileContents.length) {
638 addAnalysisError(
639 file,
640 AnalysisError(
641 lineNumber,
642 columnNumber,
643 message,
644 errorCode,
645 Line('', filename: file.path, line: lineNumber),
646 ),
647 );
Greg Spencer262f12b2019-02-15 07:48:49 -0800648 throw SampleCheckerException('Failed to parse error message: $error', file: file.path, line: lineNumber);
Greg Spencer202b0452018-11-05 07:31:35 -0800649 }
650
651 final Section actualSection = sections[file.path];
Greg Spencer262f12b2019-02-15 07:48:49 -0800652 if (actualSection == null) {
653 throw SampleCheckerException(
654 "Unknown section for ${file.path}. Maybe the temporary directory wasn't empty?",
655 file: file.path,
656 line: lineNumber,
657 );
658 }
Greg Spencer202b0452018-11-05 07:31:35 -0800659 final Line actualLine = actualSection.code[lineNumber - 1];
660
Greg Spencer262f12b2019-02-15 07:48:49 -0800661 if (actualLine?.filename == null) {
Greg Spencer202b0452018-11-05 07:31:35 -0800662 if (errorCode == 'missing_identifier' && lineNumber > 1) {
663 if (fileContents[lineNumber - 2].endsWith(',')) {
664 final Line actualLine = sections[file.path].code[lineNumber - 2];
665 addAnalysisError(
666 file,
667 AnalysisError(
668 actualLine.line,
669 actualLine.indent + fileContents[lineNumber - 2].length - 1,
670 'Unexpected comma at end of sample code.',
671 errorCode,
672 actualLine,
673 ),
674 );
675 }
676 } else {
677 addAnalysisError(
678 file,
679 AnalysisError(
680 lineNumber - 1,
681 columnNumber,
682 message,
683 errorCode,
684 actualLine,
685 ),
686 );
687 }
688 } else {
689 addAnalysisError(
690 file,
691 AnalysisError(
692 actualLine.line,
693 actualLine.indent + columnNumber,
694 message,
695 errorCode,
696 actualLine,
697 ),
698 );
699 }
Ian Hickson9ac16682017-06-12 16:52:35 -0700700 }
701 } else {
Greg Spencer202b0452018-11-05 07:31:35 -0800702 stderr.writeln('Analyzer output: $error');
703 unknownAnalyzerErrors = true;
Ian Hickson9ac16682017-06-12 16:52:35 -0700704 }
705 }
Greg Spencer202b0452018-11-05 07:31:35 -0800706 if (_exitCode == 1 && analysisErrors.isEmpty && !unknownAnalyzerErrors) {
707 _exitCode = 0;
708 }
709 if (_exitCode == 0) {
710 print('No analysis errors in samples!');
711 assert(analysisErrors.isEmpty);
712 }
713 return analysisErrors;
714 }
715
716 /// Process one block of sample code (the part inside of "```" markers).
717 /// Splits any sections denoted by "// ..." into separate blocks to be
718 /// processed separately. Uses a primitive heuristic to make sample blocks
719 /// into valid Dart code.
720 Section _processBlock(Line line, List<String> block) {
721 if (block.isEmpty) {
Greg Spencer262f12b2019-02-15 07:48:49 -0800722 throw SampleCheckerException('$line: Empty ```dart block in sample code.');
Greg Spencer202b0452018-11-05 07:31:35 -0800723 }
LongCatIsLooong712195b2019-04-01 17:27:29 -0700724 if (block.first.startsWith('new ') || block.first.startsWith(_constructorRegExp)) {
Greg Spencer202b0452018-11-05 07:31:35 -0800725 _expressionId += 1;
726 return Section.surround(line, 'dynamic expression$_expressionId = ', block.toList(), ';');
727 } else if (block.first.startsWith('await ')) {
728 _expressionId += 1;
729 return Section.surround(line, 'Future<void> expression$_expressionId() async { ', block.toList(), ' }');
730 } else if (block.first.startsWith('class ') || block.first.startsWith('enum ')) {
731 return Section.fromStrings(line, block.toList());
732 } else if ((block.first.startsWith('_') || block.first.startsWith('final ')) && block.first.contains(' = ')) {
733 _expressionId += 1;
734 return Section.surround(line, 'void expression$_expressionId() { ', block.toList(), ' }');
Ian Hickson9ac16682017-06-12 16:52:35 -0700735 } else {
Greg Spencer202b0452018-11-05 07:31:35 -0800736 final List<String> buffer = <String>[];
737 int subblocks = 0;
738 Line subline;
739 final List<Section> subsections = <Section>[];
740 for (int index = 0; index < block.length; index += 1) {
741 // Each section of the dart code that is either split by a blank line, or with '// ...' is
742 // treated as a separate code block.
743 if (block[index] == '' || block[index] == '// ...') {
744 if (subline == null)
Greg Spencer262f12b2019-02-15 07:48:49 -0800745 throw SampleCheckerException('${Line('', filename: line.filename, line: line.line + index, indent: line.indent)}: '
746 'Unexpected blank line or "// ..." line near start of subblock in sample code.');
Greg Spencer202b0452018-11-05 07:31:35 -0800747 subblocks += 1;
748 subsections.add(_processBlock(subline, buffer));
749 buffer.clear();
750 assert(buffer.isEmpty);
751 subline = null;
752 } else if (block[index].startsWith('// ')) {
753 if (buffer.length > 1) // don't include leading comments
754 buffer.add('/${block[index]}'); // so that it doesn't start with "// " and get caught in this again
755 } else {
756 subline ??= Line(
757 block[index],
758 filename: line.filename,
759 line: line.line + index,
760 indent: line.indent,
761 );
762 buffer.add(block[index]);
763 }
764 }
765 if (subblocks > 0) {
766 if (subline != null) {
767 subsections.add(_processBlock(subline, buffer));
768 }
769 // Combine all of the subsections into one section, now that they've been processed.
770 return Section.combine(subsections);
771 } else {
772 return Section.fromStrings(line, block.toList());
Devon Carew23098dd2018-05-11 07:46:48 -0700773 }
Ian Hickson9ac16682017-06-12 16:52:35 -0700774 }
775 }
Ian Hickson9ac16682017-06-12 16:52:35 -0700776}
777
Greg Spencer202b0452018-11-05 07:31:35 -0800778/// A class to represent a line of input code.
779class Line {
780 const Line(this.code, {this.filename, this.line, this.indent});
781 final String filename;
782 final int line;
783 final int indent;
784 final String code;
Alexandre Ardhuin976cffa2018-09-07 11:00:05 +0200785
Greg Spencer202b0452018-11-05 07:31:35 -0800786 String toStringWithColumn(int column) {
Greg Spencera6e90112018-11-07 20:35:10 -0800787 if (column != null && indent != null) {
Greg Spencer202b0452018-11-05 07:31:35 -0800788 return '$filename:$line:${column + indent}: $code';
Ian Hickson9ac16682017-06-12 16:52:35 -0700789 }
Greg Spencer202b0452018-11-05 07:31:35 -0800790 return toString();
791 }
792
793 @override
794 String toString() => '$filename:$line: $code';
795}
796
Greg Spencerfabf4e32020-01-08 15:28:02 -0800797/// A class to represent a section of sample code, marked by "{@tool snippet}...{@end-tool}".
Greg Spencer202b0452018-11-05 07:31:35 -0800798class Section {
799 const Section(this.code);
800 factory Section.combine(List<Section> sections) {
Alexandre Ardhuin758009b2019-07-02 21:11:56 +0200801 final List<Line> code = sections
802 .expand((Section section) => section.code)
803 .toList();
Greg Spencer202b0452018-11-05 07:31:35 -0800804 return Section(code);
805 }
806 factory Section.fromStrings(Line firstLine, List<String> code) {
807 final List<Line> codeLines = <Line>[];
808 for (int i = 0; i < code.length; ++i) {
809 codeLines.add(
810 Line(
811 code[i],
812 filename: firstLine.filename,
813 line: firstLine.line + i,
814 indent: firstLine.indent,
815 ),
816 );
817 }
818 return Section(codeLines);
819 }
820 factory Section.surround(Line firstLine, String prefix, List<String> code, String postfix) {
821 assert(prefix != null);
822 assert(postfix != null);
823 final List<Line> codeLines = <Line>[];
824 for (int i = 0; i < code.length; ++i) {
825 codeLines.add(
826 Line(
827 code[i],
828 filename: firstLine.filename,
829 line: firstLine.line + i,
830 indent: firstLine.indent,
831 ),
832 );
833 }
Alexandre Ardhuin919dcf52019-06-27 21:23:16 +0200834 return Section(<Line>[
835 Line(prefix),
836 ...codeLines,
837 Line(postfix),
838 ]);
Greg Spencer202b0452018-11-05 07:31:35 -0800839 }
840 Line get start => code.firstWhere((Line line) => line.filename != null);
841 final List<Line> code;
842}
843
Greg Spencerfabf4e32020-01-08 15:28:02 -0800844/// A class to represent a sample in the dartdoc comments, marked by
845/// "{@tool sample ...}...{@end-tool}". Samples are processed separately from
846/// regular snippets, because they must be injected into templates in order to be
Greg Spencer202b0452018-11-05 07:31:35 -0800847/// analyzed.
Greg Spencerfabf4e32020-01-08 15:28:02 -0800848class Sample {
849 Sample({this.start, List<String> input, List<String> args, this.serial}) {
Alexandre Ardhuin919dcf52019-06-27 21:23:16 +0200850 this.input = input.toList();
851 this.args = args.toList();
Greg Spencer202b0452018-11-05 07:31:35 -0800852 }
853 final Line start;
854 final int serial;
855 List<String> input;
856 List<String> args;
857 List<String> contents;
858
859 @override
860 String toString() {
Greg Spencerfabf4e32020-01-08 15:28:02 -0800861 final StringBuffer buf = StringBuffer('sample ${args.join(' ')}\n');
Greg Spencer202b0452018-11-05 07:31:35 -0800862 int count = start.line;
Alexandre Ardhuin4f9b6cf2020-01-07 16:32:04 +0100863 for (final String line in input) {
Greg Spencer202b0452018-11-05 07:31:35 -0800864 buf.writeln(' ${count.toString().padLeft(4, ' ')}: $line');
865 count++;
866 }
867 return buf.toString();
868 }
869}
870
871/// A class representing an analysis error along with the context of the error.
872///
873/// Changes how it converts to a string based on the source of the error.
874class AnalysisError {
875 const AnalysisError(
876 this.line,
877 this.column,
878 this.message,
879 this.errorCode,
880 this.source, {
Greg Spencerfabf4e32020-01-08 15:28:02 -0800881 this.sample,
Greg Spencer202b0452018-11-05 07:31:35 -0800882 });
883
884 final int line;
885 final int column;
886 final String message;
887 final String errorCode;
888 final Line source;
Greg Spencerfabf4e32020-01-08 15:28:02 -0800889 final Sample sample;
Greg Spencer202b0452018-11-05 07:31:35 -0800890
891 @override
892 String toString() {
893 if (source != null) {
894 return '${source.toStringWithColumn(column)}\n>>> $message ($errorCode)';
Greg Spencerfabf4e32020-01-08 15:28:02 -0800895 } else if (sample != null) {
896 return 'In sample starting at '
897 '${sample.start.filename}:${sample.start.line}:${sample.contents[line - 1]}\n'
Greg Spencer202b0452018-11-05 07:31:35 -0800898 '>>> $message ($errorCode)';
Ian Hickson9ac16682017-06-12 16:52:35 -0700899 } else {
Greg Spencer202b0452018-11-05 07:31:35 -0800900 return '<source unknown>:$line:$column\n>>> $message ($errorCode)';
Ian Hickson9ac16682017-06-12 16:52:35 -0700901 }
902 }
Ian Hickson9ac16682017-06-12 16:52:35 -0700903}