Ian Hickson | 449f4a6 | 2019-11-27 15:04:02 -0800 | [diff] [blame] | 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
Ian Hickson | a516a24 | 2019-12-16 12:33:01 -0800 | [diff] [blame] | 5 | // 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 Hickson | 28352c3 | 2018-03-19 16:42:30 -0700 | [diff] [blame] | 9 | |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 10 | import 'dart:io'; |
| 11 | |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 12 | import 'package:args/args.dart'; |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 13 | import 'package:path/path.dart' as path; |
| 14 | |
| 15 | final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script)))); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 16 | final String _defaultFlutterPackage = path.join(_flutterRoot, 'packages', 'flutter', 'lib'); |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 17 | final String _flutter = path.join(_flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter'); |
| 18 | |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 19 | void main(List<String> arguments) { |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 20 | 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 Hickson | a516a24 | 2019-12-16 12:33:01 -0800 | [diff] [blame] | 25 | 'directory in the system temp folder. If specified, will not be ' |
| 26 | 'automatically removed at the end of execution.', |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 27 | ); |
| 28 | argParser.addFlag( |
Greg Spencer | a3186fb | 2019-11-19 15:16:12 -0800 | [diff] [blame] | 29 | 'verbose', |
| 30 | defaultsTo: false, |
| 31 | negatable: false, |
| 32 | help: 'Print verbose output for the analysis process.', |
| 33 | ); |
| 34 | argParser.addFlag( |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 35 | '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 Ardhuin | ec1a015 | 2019-12-05 22:34:06 +0100 | [diff] [blame] | 43 | if (parsedArguments['help'] as bool) { |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 44 | print(argParser.usage); |
Ian Hickson | a516a24 | 2019-12-16 12:33:01 -0800 | [diff] [blame] | 45 | print('See dev/snippets/README.md for documentation.'); |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 46 | exit(0); |
| 47 | } |
| 48 | |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 49 | Directory flutterPackage; |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 50 | if (parsedArguments.rest.length == 1) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 51 | // Used for testing. |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 52 | flutterPackage = Directory(parsedArguments.rest.single); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 53 | } else { |
| 54 | flutterPackage = Directory(_defaultFlutterPackage); |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 55 | } |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 56 | |
| 57 | Directory tempDirectory; |
| 58 | if (parsedArguments.wasParsed('temp')) { |
Alexandre Ardhuin | ec1a015 | 2019-12-05 22:34:06 +0100 | [diff] [blame] | 59 | 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 Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 62 | 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 Ardhuin | ec1a015 | 2019-12-05 22:34:06 +0100 | [diff] [blame] | 73 | exitCode = SampleChecker( |
| 74 | flutterPackage, |
| 75 | tempDirectory: tempDirectory, |
| 76 | verbose: parsedArguments['verbose'] as bool, |
| 77 | ).checkSamples(); |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 78 | } on SampleCheckerException catch (e) { |
| 79 | stderr.write(e); |
| 80 | exit(1); |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | class 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 Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 98 | } |
| 99 | } |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 100 | } |
| 101 | |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 102 | /// 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 Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 110 | /// 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 Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 114 | /// don't necessarily match. It does, however, print the source of the |
| 115 | /// problematic line. |
| 116 | class SampleChecker { |
Greg Spencer | a3186fb | 2019-11-19 15:16:12 -0800 | [diff] [blame] | 117 | SampleChecker(this._flutterPackage, {Directory tempDirectory, this.verbose = false}) |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 118 | : _tempDirectory = tempDirectory, |
| 119 | _keepTmp = tempDirectory != null { |
| 120 | _tempDirectory ??= Directory.systemTemp.createTempSync('flutter_analyze_sample_code.'); |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 121 | } |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 122 | |
| 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 Spencer | a3186fb | 2019-11-19 15:16:12 -0800 | [diff] [blame] | 130 | static final RegExp _dartDocSampleBeginRegex = RegExp(r'{@tool (sample|snippet|dartpad)(?:| ([^}]*))}'); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 131 | |
| 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 Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 136 | static final RegExp _codeBlockStartRegex = RegExp(r'///\s+```dart.*$'); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 137 | |
| 138 | /// A RegExp that matches the end of a code block within dartdoc. |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 139 | static final RegExp _codeBlockEndRegex = RegExp(r'///\s+```\s*$'); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 140 | |
| 141 | /// A RegExp that matches a Dart constructor. |
LongCatIsLooong | 712195b | 2019-04-01 17:27:29 -0700 | [diff] [blame] | 142 | static final RegExp _constructorRegExp = RegExp(r'(const\s+)?_*[A-Z][a-zA-Z0-9<>._]*\('); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 143 | |
Greg Spencer | a3186fb | 2019-11-19 15:16:12 -0800 | [diff] [blame] | 144 | /// Whether or not to print verbose output. |
| 145 | final bool verbose; |
| 146 | |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 147 | /// Whether or not to keep the temp directory around after running. |
| 148 | /// |
| 149 | /// Defaults to false. |
| 150 | final bool _keepTmp; |
| 151 | |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 152 | /// The temporary directory where all output is written. This will be deleted |
| 153 | /// automatically if there are no errors. |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 154 | Directory _tempDirectory; |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 155 | |
| 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 Collins | 4a110b6 | 2018-12-12 09:40:18 -0800 | [diff] [blame] | 172 | final String platformScriptPath = path.dirname(path.fromUri(Platform.script)); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 173 | return path.canonicalize(path.join(platformScriptPath, '..', 'snippets', 'lib', 'main.dart')); |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 174 | } |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 175 | |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 176 | /// 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 Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 182 | static List<File> _listDartFiles(Directory directory, {bool recursive = false}) { |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 183 | return directory.listSync(recursive: recursive, followLinks: false).whereType<File>().where((File file) => path.extension(file.path) == '.dart').toList(); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 184 | } |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 185 | |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 186 | /// Computes the headers needed for each sample file. |
| 187 | List<Line> get headers { |
Alexandre Ardhuin | df4bf45 | 2019-09-17 16:23:44 +0200 | [diff] [blame] | 188 | 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 Ardhuin | 4f9b6cf | 2020-01-07 16:32:04 +0100 | [diff] [blame] | 196 | for (final File file in _listDartFiles(Directory(_defaultFlutterPackage))) ...<String>[ |
Alexandre Ardhuin | df4bf45 | 2019-09-17 16:23:44 +0200 | [diff] [blame] | 197 | '', |
| 198 | '// ${file.path}', |
| 199 | "import 'package:flutter/${path.basename(file.path)}';", |
| 200 | ], |
| 201 | ].map<Line>((String code) => Line(code)).toList(); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 202 | } |
| 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 Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 212 | final Map<String, Sample> snippets = <String, Sample>{}; |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 213 | _extractSamples(sections, snippets); |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 214 | errors = _analyze(_tempDirectory, sections, snippets); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 215 | } finally { |
| 216 | if (errors.isNotEmpty) { |
Alexandre Ardhuin | 4f9b6cf | 2020-01-07 16:32:04 +0100 | [diff] [blame] | 217 | for (final String filePath in errors.keys) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 218 | errors[filePath].forEach(stderr.writeln); |
| 219 | } |
| 220 | stderr.writeln('\nFound ${errors.length} sample code errors.'); |
| 221 | } |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 222 | if (_keepTmp) { |
| 223 | print('Leaving temporary directory ${_tempDirectory.path} around for your perusal.'); |
| 224 | } else { |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 225 | try { |
| 226 | _tempDirectory.deleteSync(recursive: true); |
| 227 | } on FileSystemException catch (e) { |
| 228 | stderr.writeln('Failed to delete ${_tempDirectory.path}: $e'); |
| 229 | } |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 230 | } |
| 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 Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 245 | String sampleId = path.split(filename).join('.'); |
| 246 | sampleId = path.basenameWithoutExtension(sampleId); |
| 247 | sampleId = '$prefix.$sampleId.$start'; |
| 248 | return sampleId; |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 249 | } |
| 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 Spencer | 094f93d | 2018-11-07 08:29:14 -0800 | [diff] [blame] | 254 | final String workingDirectory = path.join(_flutterRoot, 'dev', 'docs'); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 255 | if (_snippetsSnapshotPath == null) { |
| 256 | _snippetsSnapshotPath = '$_snippetsExecutable.snapshot'; |
| 257 | return Process.runSync( |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 258 | _dartExecutable, |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 259 | <String>[ |
| 260 | '--snapshot=$_snippetsSnapshotPath', |
| 261 | '--snapshot-kind=app-jit', |
Janice Collins | 4a110b6 | 2018-12-12 09:40:18 -0800 | [diff] [blame] | 262 | path.canonicalize(_snippetsExecutable), |
Alexandre Ardhuin | 919dcf5 | 2019-06-27 21:23:16 +0200 | [diff] [blame] | 263 | ...args, |
| 264 | ], |
Greg Spencer | 094f93d | 2018-11-07 08:29:14 -0800 | [diff] [blame] | 265 | workingDirectory: workingDirectory, |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 266 | ); |
| 267 | } else { |
| 268 | return Process.runSync( |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 269 | _dartExecutable, |
Alexandre Ardhuin | 919dcf5 | 2019-06-27 21:23:16 +0200 | [diff] [blame] | 270 | <String>[path.canonicalize(_snippetsSnapshotPath), ...args], |
Greg Spencer | 094f93d | 2018-11-07 08:29:14 -0800 | [diff] [blame] | 271 | workingDirectory: workingDirectory, |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 272 | ); |
| 273 | } |
| 274 | } |
| 275 | |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 276 | /// Writes out the given sample to an output file in the [_tempDirectory] and |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 277 | /// returns the output file. |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 278 | File _writeSample(Sample sample) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 279 | // Generate the snippet. |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 280 | final String sampleId = _createNameFromSource('sample', sample.start.filename, sample.start.line); |
| 281 | final String inputName = '$sampleId.input'; |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 282 | // Now we have a filename like 'lib.src.material.foo_widget.123.dart' for each snippet. |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 283 | final File inputFile = File(path.join(_tempDirectory.path, inputName))..createSync(recursive: true); |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 284 | inputFile.writeAsStringSync(sample.input.join('\n')); |
| 285 | final File outputFile = File(path.join(_tempDirectory.path, '$sampleId.dart')); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 286 | final List<String> args = <String>[ |
| 287 | '--output=${outputFile.absolute.path}', |
| 288 | '--input=${inputFile.absolute.path}', |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 289 | ...sample.args, |
Alexandre Ardhuin | 919dcf5 | 2019-06-27 21:23:16 +0200 | [diff] [blame] | 290 | ]; |
Ian Hickson | a516a24 | 2019-12-16 12:33:01 -0800 | [diff] [blame] | 291 | if (verbose) |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 292 | print('Generating sample for ${sample.start?.filename}:${sample.start?.line}'); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 293 | final ProcessResult process = _runSnippetsScript(args); |
Ian Hickson | a516a24 | 2019-12-16 12:33:01 -0800 | [diff] [blame] | 294 | if (verbose) |
Greg Spencer | a3186fb | 2019-11-19 15:16:12 -0800 | [diff] [blame] | 295 | stderr.write('${process.stderr}'); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 296 | if (process.exitCode != 0) { |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 297 | throw SampleCheckerException( |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 298 | 'Unable to create sample for ${sample.start.filename}:${sample.start.line} ' |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 299 | '(using input from ${inputFile.path}):\n${process.stdout}\n${process.stderr}', |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 300 | file: sample.start.filename, |
| 301 | line: sample.start.line, |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 302 | ); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 303 | } |
| 304 | return outputFile; |
| 305 | } |
| 306 | |
| 307 | /// Extracts the samples from the Dart files in [_flutterPackage], writes them |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 308 | /// to disk, and adds them to the appropriate [sectionMap] or [sampleMap]. |
| 309 | void _extractSamples(Map<String, Section> sectionMap, Map<String, Sample> sampleMap) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 310 | final List<Section> sections = <Section>[]; |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 311 | final List<Sample> samples = <Sample>[]; |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 312 | |
Alexandre Ardhuin | 4f9b6cf | 2020-01-07 16:32:04 +0100 | [diff] [blame] | 313 | for (final File file in _listDartFiles(_flutterPackage, recursive: true)) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 314 | final String relativeFilePath = path.relative(file.path, from: _flutterPackage.path); |
| 315 | final List<String> sampleLines = file.readAsLinesSync(); |
| 316 | final List<Section> preambleSections = <Section>[]; |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 317 | // Whether or not we're in the file-wide preamble section ("Examples can assume"). |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 318 | bool inPreamble = false; |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 319 | // Whether or not we're in a code sample |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 320 | bool inSampleSection = false; |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 321 | // Whether or not we're in a snippet code sample (with template) specifically. |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 322 | bool inSnippet = false; |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 323 | // Whether or not we're in a '```dart' segment. |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 324 | bool inDart = false; |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 325 | int lineNumber = 0; |
| 326 | final List<String> block = <String>[]; |
| 327 | List<String> snippetArgs = <String>[]; |
| 328 | Line startLine; |
Alexandre Ardhuin | 4f9b6cf | 2020-01-07 16:32:04 +0100 | [diff] [blame] | 329 | for (final String line in sampleLines) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 330 | lineNumber += 1; |
| 331 | final String trimmedLine = line.trim(); |
| 332 | if (inSnippet) { |
| 333 | if (!trimmedLine.startsWith(_dartDocPrefix)) { |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 334 | throw SampleCheckerException('Snippet section unterminated.', file: relativeFilePath, line: lineNumber); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 335 | } |
| 336 | if (_dartDocSampleEndRegex.hasMatch(trimmedLine)) { |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 337 | samples.add( |
| 338 | Sample( |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 339 | start: startLine, |
| 340 | input: block, |
| 341 | args: snippetArgs, |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 342 | serial: samples.length, |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 343 | ), |
| 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 Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 358 | throw SampleCheckerException('Unexpected content in sample code preamble.', file: relativeFilePath, line: lineNumber); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 359 | } else { |
| 360 | block.add(line.substring(3)); |
| 361 | } |
| 362 | } else if (inSampleSection) { |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 363 | if (_dartDocSampleEndRegex.hasMatch(trimmedLine)) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 364 | if (inDart) { |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 365 | throw SampleCheckerException("Dart section didn't terminate before end of sample", file: relativeFilePath, line: lineNumber); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 366 | } |
| 367 | inSampleSection = false; |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 368 | } |
| 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 Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 375 | } else { |
Alexandre Ardhuin | 9c31f9f | 2019-07-01 07:05:42 +0200 | [diff] [blame] | 376 | sections.add(Section.combine(preambleSections..add(processed))); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 377 | } |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 378 | 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 Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 391 | } |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 392 | } 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 Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 401 | } |
| 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 Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 409 | } else if (sampleMatch != null) { |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 410 | inSnippet = sampleMatch != null && (sampleMatch[1] == 'sample' || sampleMatch[1] == 'dartpad'); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 411 | 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 Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 426 | } else if (RegExp(r'///\s*#+\s+[Ss]ample\s+[Cc]ode:?$').hasMatch(trimmedLine)) { |
| 427 | throw SampleCheckerException( |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 428 | "Found deprecated '## Sample code' section: use {@tool snippet}...{@end-tool} instead.", |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 429 | file: relativeFilePath, |
| 430 | line: lineNumber, |
| 431 | ); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 432 | } |
| 433 | } |
| 434 | } |
| 435 | } |
| 436 | print('Found ${sections.length} sample code sections.'); |
Alexandre Ardhuin | 4f9b6cf | 2020-01-07 16:32:04 +0100 | [diff] [blame] | 437 | for (final Section section in sections) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 438 | sectionMap[_writeSection(section).path] = section; |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 439 | } |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 440 | for (final Sample sample in samples) { |
| 441 | final File snippetFile = _writeSample(sample); |
| 442 | sample.contents = snippetFile.readAsLinesSync(); |
| 443 | sampleMap[snippetFile.absolute.path] = sample; |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 444 | } |
| 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 Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 491 | pubSpec.writeAsStringSync(''' |
| 492 | name: analyze_sample_code |
| 493 | dependencies: |
| 494 | flutter: |
| 495 | sdk: flutter |
Ian Hickson | ded3905 | 2018-03-11 03:19:18 -0700 | [diff] [blame] | 496 | flutter_test: |
| 497 | sdk: flutter |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 498 | '''); |
Alexandre Ardhuin | 3c58195 | 2018-09-07 09:13:13 +0200 | [diff] [blame] | 499 | analysisOptions.writeAsStringSync(''' |
| 500 | linter: |
| 501 | rules: |
| 502 | - unnecessary_const |
Alexandre Ardhuin | 976cffa | 2018-09-07 11:00:05 +0200 | [diff] [blame] | 503 | - unnecessary_new |
Alexandre Ardhuin | 3c58195 | 2018-09-07 09:13:13 +0200 | [diff] [blame] | 504 | '''); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 505 | } |
| 506 | |
| 507 | /// Writes out a sample section to the disk and returns the file. |
| 508 | File _writeSection(Section section) { |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 509 | final String sectionId = _createNameFromSource('snippet', section.start.filename, section.start.line); |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 510 | final File outputFile = File(path.join(_tempDirectory.path, '$sectionId.dart'))..createSync(recursive: true); |
Alexandre Ardhuin | 758009b | 2019-07-02 21:11:56 +0200 | [diff] [blame] | 511 | final List<Line> mainContents = <Line>[ |
| 512 | ...headers, |
| 513 | const Line(''), |
| 514 | Line('// From: ${section.start.filename}:${section.start.line}'), |
| 515 | ...section.code, |
| 516 | ]; |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 517 | 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 Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 523 | print('Starting analysis of code samples.'); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 524 | _createConfigurationFiles(directory); |
| 525 | final ProcessResult result = Process.runSync( |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 526 | _flutter, |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 527 | <String>['--no-wrap', 'analyze', '--no-preamble', '--no-congratulate', '.'], |
| 528 | workingDirectory: directory.absolute.path, |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 529 | ); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 530 | 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 Hickson | ad1eaff | 2018-08-20 12:51:07 -0700 | [diff] [blame] | 540 | } |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 541 | 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 Thomsen | 7ae3caf | 2019-05-21 16:38:58 +0200 | [diff] [blame] | 547 | if (stdout.isNotEmpty && stdout.first.startsWith('Running "flutter pub get" in ')) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 548 | 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 Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 559 | Map<String, Sample> samples, |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 560 | ) { |
| 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 Hickson | ad1eaff | 2018-08-20 12:51:07 -0700 | [diff] [blame] | 571 | final String kBullet = Platform.isWindows ? ' - ' : ' • '; |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 572 | // 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 Ardhuin | 4f9b6cf | 2020-01-07 16:32:04 +0100 | [diff] [blame] | 579 | for (final String error in errors) { |
Ian Hickson | ad1eaff | 2018-08-20 12:51:07 -0700 | [diff] [blame] | 580 | final Match parts = errorPattern.matchAsPrefix(error); |
| 581 | if (parts != null) { |
| 582 | final String message = parts[2]; |
Greg Spencer | bcd60fa | 2019-02-08 10:12:14 -0800 | [diff] [blame] | 583 | final File file = File(path.join(_tempDirectory.path, parts[3])); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 584 | 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 Hickson | ad1eaff | 2018-08-20 12:51:07 -0700 | [diff] [blame] | 587 | final String line = parts[4]; |
| 588 | final String column = parts[5]; |
| 589 | final String errorCode = parts[6]; |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 590 | final int lineNumber = int.parse(line, radix: 10) - (isSnippet ? headerLength : 0); |
Ian Hickson | ad1eaff | 2018-08-20 12:51:07 -0700 | [diff] [blame] | 591 | final int columnNumber = int.parse(column, radix: 10); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 592 | if (lineNumber < 0 && errorCode == 'unused_import') { |
| 593 | // We don't care about unused imports. |
| 594 | continue; |
Alexander Aprelev | e0cd42e | 2018-04-12 16:28:01 -0700 | [diff] [blame] | 595 | } |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 596 | |
| 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 Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 613 | throw SampleCheckerException( |
| 614 | 'Cannot analyze dartdocs; analysis errors exist: $error', |
| 615 | file: file.path, |
| 616 | line: lineNumber, |
| 617 | ); |
Ian Hickson | a29d723 | 2018-01-20 01:42:55 -0800 | [diff] [blame] | 618 | } |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 619 | |
Ian Hickson | 6d134e0 | 2018-09-23 00:43:05 -0700 | [diff] [blame] | 620 | if (errorCode == 'unused_element' || errorCode == 'unused_local_variable') { |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 621 | // We don't really care if sample code isn't used! |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 622 | continue; |
| 623 | } |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 624 | if (isSample) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 625 | addAnalysisError( |
| 626 | file, |
| 627 | AnalysisError( |
| 628 | lineNumber, |
| 629 | columnNumber, |
| 630 | message, |
| 631 | errorCode, |
| 632 | null, |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 633 | sample: samples[file.path], |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 634 | ), |
| 635 | ); |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 636 | } else { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 637 | 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 Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 648 | throw SampleCheckerException('Failed to parse error message: $error', file: file.path, line: lineNumber); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 649 | } |
| 650 | |
| 651 | final Section actualSection = sections[file.path]; |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 652 | 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 Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 659 | final Line actualLine = actualSection.code[lineNumber - 1]; |
| 660 | |
Greg Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 661 | if (actualLine?.filename == null) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 662 | 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 Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 700 | } |
| 701 | } else { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 702 | stderr.writeln('Analyzer output: $error'); |
| 703 | unknownAnalyzerErrors = true; |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 704 | } |
| 705 | } |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 706 | 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 Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 722 | throw SampleCheckerException('$line: Empty ```dart block in sample code.'); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 723 | } |
LongCatIsLooong | 712195b | 2019-04-01 17:27:29 -0700 | [diff] [blame] | 724 | if (block.first.startsWith('new ') || block.first.startsWith(_constructorRegExp)) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 725 | _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 Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 735 | } else { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 736 | 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 Spencer | 262f12b | 2019-02-15 07:48:49 -0800 | [diff] [blame] | 745 | 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 Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 747 | 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 Carew | 23098dd | 2018-05-11 07:46:48 -0700 | [diff] [blame] | 773 | } |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 774 | } |
| 775 | } |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 776 | } |
| 777 | |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 778 | /// A class to represent a line of input code. |
| 779 | class 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 Ardhuin | 976cffa | 2018-09-07 11:00:05 +0200 | [diff] [blame] | 785 | |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 786 | String toStringWithColumn(int column) { |
Greg Spencer | a6e9011 | 2018-11-07 20:35:10 -0800 | [diff] [blame] | 787 | if (column != null && indent != null) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 788 | return '$filename:$line:${column + indent}: $code'; |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 789 | } |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 790 | return toString(); |
| 791 | } |
| 792 | |
| 793 | @override |
| 794 | String toString() => '$filename:$line: $code'; |
| 795 | } |
| 796 | |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 797 | /// A class to represent a section of sample code, marked by "{@tool snippet}...{@end-tool}". |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 798 | class Section { |
| 799 | const Section(this.code); |
| 800 | factory Section.combine(List<Section> sections) { |
Alexandre Ardhuin | 758009b | 2019-07-02 21:11:56 +0200 | [diff] [blame] | 801 | final List<Line> code = sections |
| 802 | .expand((Section section) => section.code) |
| 803 | .toList(); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 804 | 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 Ardhuin | 919dcf5 | 2019-06-27 21:23:16 +0200 | [diff] [blame] | 834 | return Section(<Line>[ |
| 835 | Line(prefix), |
| 836 | ...codeLines, |
| 837 | Line(postfix), |
| 838 | ]); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 839 | } |
| 840 | Line get start => code.firstWhere((Line line) => line.filename != null); |
| 841 | final List<Line> code; |
| 842 | } |
| 843 | |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 844 | /// 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 Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 847 | /// analyzed. |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 848 | class Sample { |
| 849 | Sample({this.start, List<String> input, List<String> args, this.serial}) { |
Alexandre Ardhuin | 919dcf5 | 2019-06-27 21:23:16 +0200 | [diff] [blame] | 850 | this.input = input.toList(); |
| 851 | this.args = args.toList(); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 852 | } |
| 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 Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 861 | final StringBuffer buf = StringBuffer('sample ${args.join(' ')}\n'); |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 862 | int count = start.line; |
Alexandre Ardhuin | 4f9b6cf | 2020-01-07 16:32:04 +0100 | [diff] [blame] | 863 | for (final String line in input) { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 864 | 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. |
| 874 | class AnalysisError { |
| 875 | const AnalysisError( |
| 876 | this.line, |
| 877 | this.column, |
| 878 | this.message, |
| 879 | this.errorCode, |
| 880 | this.source, { |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 881 | this.sample, |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 882 | }); |
| 883 | |
| 884 | final int line; |
| 885 | final int column; |
| 886 | final String message; |
| 887 | final String errorCode; |
| 888 | final Line source; |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 889 | final Sample sample; |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 890 | |
| 891 | @override |
| 892 | String toString() { |
| 893 | if (source != null) { |
| 894 | return '${source.toStringWithColumn(column)}\n>>> $message ($errorCode)'; |
Greg Spencer | fabf4e3 | 2020-01-08 15:28:02 -0800 | [diff] [blame] | 895 | } else if (sample != null) { |
| 896 | return 'In sample starting at ' |
| 897 | '${sample.start.filename}:${sample.start.line}:${sample.contents[line - 1]}\n' |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 898 | '>>> $message ($errorCode)'; |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 899 | } else { |
Greg Spencer | 202b045 | 2018-11-05 07:31:35 -0800 | [diff] [blame] | 900 | return '<source unknown>:$line:$column\n>>> $message ($errorCode)'; |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 901 | } |
| 902 | } |
Ian Hickson | 9ac1668 | 2017-06-12 16:52:35 -0700 | [diff] [blame] | 903 | } |