Unverified Commit 202b045b authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Rewrite the analyze-sample-code script to also analyze snippets (#23893)

This rewrites the sample code analysis script to be a little less of a hack (but still not pretty), and to handle snippets as well.

It also changes the semantics of how sample code is handled: the namespace for the sample code is now limited to the file that it appears in, so some additional "Examples can assume:" blocks were added. The upside of this is that there will be far fewer name collisions.

I fixed the output too: no longer will you get 4000 lines of numbered output with the error at the top and have to grep for the actual problem. It gives the filename and line number of the original location of the code (in the comment in the tree), and prints out the source code on the line that caused the problem along with the error.

For snippets, it prints out the location of the start of the snippet and the source code line that causes the problem. It can't print out the original line, because snippets get formatted when they are written, so the line might not be in the same place.
parent 34bc1e3c
...@@ -11,6 +11,11 @@ ...@@ -11,6 +11,11 @@
// Only code in "## Sample code" or "### Sample code" sections is examined. // Only code in "## Sample code" or "### Sample code" sections is examined.
// Subheadings can also be specified, as in "## Sample code: foo". // Subheadings can also be specified, as in "## Sample code: foo".
// //
// Additionally, code inside of dartdoc snippet and sample blocks
// ({@tool snippet ...}{@end-tool}, and {@tool sample ...}{@end-tool})
// is recognized as sample code. Snippets are processed as separate programs,
// and samples are processed in the same way as "## Sample code" blocks are.
//
// There are several kinds of sample code you can specify: // There are several kinds of sample code you can specify:
// //
// * Constructor calls, typically showing what might exist in a build method. // * Constructor calls, typically showing what might exist in a build method.
...@@ -38,8 +43,6 @@ ...@@ -38,8 +43,6 @@
// top level. Instead, wrap it in a function or even a whole class, or make it a // top level. Instead, wrap it in a function or even a whole class, or make it a
// valid variable declaration. // valid variable declaration.
import 'dart:async';
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
...@@ -47,165 +50,385 @@ import 'package:path/path.dart' as path; ...@@ -47,165 +50,385 @@ import 'package:path/path.dart' as path;
// To run this: bin/cache/dart-sdk/bin/dart dev/bots/analyze-sample-code.dart // To run this: bin/cache/dart-sdk/bin/dart dev/bots/analyze-sample-code.dart
final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script)))); final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script))));
final String _defaultFlutterPackage = path.join(_flutterRoot, 'packages', 'flutter', 'lib');
final String _flutter = path.join(_flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter'); final String _flutter = path.join(_flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter');
class Line { void main(List<String> arguments) {
const Line(this.filename, this.line, this.indent); Directory flutterPackage;
final String filename; if (arguments.length == 1) {
final int line; // Used for testing.
final int indent; flutterPackage = Directory(arguments.single);
Line get next => this + 1; } else {
Line operator +(int count) { flutterPackage = Directory(_defaultFlutterPackage);
if (count == 0)
return this;
return Line(filename, line + count, indent);
}
@override
String toString([int column]) {
if (column != null)
return '$filename:$line:${column + indent}';
return '$filename:$line';
} }
exitCode = SampleChecker(flutterPackage).checkSamples();
} }
class Section { /// Checks samples and code snippets for analysis errors.
const Section(this.start, this.preamble, this.code, this.postamble); ///
final Line start; /// Extracts dartdoc content from flutter package source code, identifies code
final String preamble; /// sections, and writes them to a temporary directory, where 'flutter analyze'
final List<String> code; /// is used to analyze the sources for problems. If problems are found, the
final String postamble; /// error output from the analyzer is parsed for details, and the problem
Iterable<String> get strings sync* { /// locations are translated back to the source location.
if (preamble != null) { ///
assert(!preamble.contains('\n')); /// For snippets, the snippets are generated using the snippets tool, and they
yield preamble; /// are analyzed with the samples. If errors are found in snippets, then the
} /// line number of the start of the snippet is given instead of the actual error
assert(!code.any((String line) => line.contains('\n'))); /// line, since snippets get reformatted when written, and the line numbers
yield* code; /// don't necessarily match. It does, however, print the source of the
if (postamble != null) { /// problematic line.
assert(!postamble.contains('\n')); class SampleChecker {
yield postamble; SampleChecker(this._flutterPackage) {
} _tempDir = Directory.systemTemp.createTempSync('flutter_analyze_sample_code.');
}
List<Line> get lines {
final List<Line> result = List<Line>.generate(code.length, (int index) => start + index);
if (preamble != null)
result.insert(0, null);
if (postamble != null)
result.add(null);
return result;
} }
}
const String kDartDocPrefix = '///'; /// The prefix of each comment line
const String kDartDocPrefixWithSpace = '$kDartDocPrefix '; static const String _dartDocPrefix = '///';
/// The prefix of each comment line with a space appended.
static const String _dartDocPrefixWithSpace = '$_dartDocPrefix ';
/// A RegExp that matches the beginning of a dartdoc snippet or sample.
static final RegExp _dartDocSampleBeginRegex = RegExp(r'{@tool (sample|snippet)(?:| ([^}]*))}');
/// A RegExp that matches the end of a dartdoc snippet or sample.
static final RegExp _dartDocSampleEndRegex = RegExp(r'{@end-tool}');
/// A RegExp that matches the start of a code block within dartdoc.
static final RegExp _codeBlockStartRegex = RegExp(r'/// ```dart.*$');
/// A RegExp that matches the end of a code block within dartdoc.
static final RegExp _codeBlockEndRegex = RegExp(r'/// ```\s*$');
/// A RegExp that matches a Dart constructor.
static final RegExp _constructorRegExp = RegExp(r'[A-Z][a-zA-Z0-9<>.]*\(');
/// The temporary directory where all output is written. This will be deleted
/// automatically if there are no errors.
Directory _tempDir;
/// The package directory for the flutter package within the flutter root dir.
final Directory _flutterPackage;
Future<void> main(List<String> arguments) async { /// A serial number so that we can create unique expression names when we
final Directory tempDir = Directory.systemTemp.createTempSync('flutter_analyze_sample_code.'); /// generate them.
int exitCode = 1; int _expressionId = 0;
bool keepMain = false;
/// The exit code from the analysis process.
int _exitCode = 0;
// Once the snippets tool has been precompiled by Dart, this contains the AOT
// snapshot.
String _snippetsSnapshotPath;
/// Finds the location of the snippets script.
String get _snippetsExecutable {
final String platformScriptPath = path.dirname(Platform.script.toFilePath());
return path.canonicalize(path.join(platformScriptPath, '..', 'snippets', 'lib', 'main.dart'));
}
static List<File> _listDartFiles(Directory directory, {bool recursive = false}) {
return directory.listSync(recursive: recursive, followLinks: false)
.whereType<File>()
.where((File file) => path.extension(file.path) == '.dart').toList();
}
/// Computes the headers needed for each sample file.
List<Line> get headers {
if (_headers == null) {
final List<String> buffer = <String>[]; final List<String> buffer = <String>[];
buffer.add('// generated code');
buffer.add('import \'dart:async\';');
buffer.add('import \'dart:convert\';');
buffer.add('import \'dart:math\' as math;');
buffer.add('import \'dart:typed_data\';');
buffer.add('import \'dart:ui\' as ui;');
buffer.add('import \'package:flutter_test/flutter_test.dart\';');
for (File file in _listDartFiles(Directory(_defaultFlutterPackage))) {
buffer.add('');
buffer.add('// ${file.path}');
buffer.add('import \'package:flutter/${path.basename(file.path)}\';');
}
_headers = buffer.map<Line>((String code) => Line(code)).toList();
}
return _headers;
}
List<Line> _headers;
/// Checks all the samples in the Dart files in [_flutterPackage] for errors.
int checkSamples() {
_exitCode = 0;
Map<String, List<AnalysisError>> errors = <String, List<AnalysisError>>{};
try { try {
final File mainDart = File(path.join(tempDir.path, 'main.dart')); final Map<String, Section> sections = <String, Section>{};
final File pubSpec = File(path.join(tempDir.path, 'pubspec.yaml')); final Map<String, Snippet> snippets = <String, Snippet>{};
final File analysisOptions = File(path.join(tempDir.path, 'analysis_options.yaml')); _extractSamples(sections, snippets);
Directory flutterPackage; errors = _analyze(_tempDir, sections, snippets);
if (arguments.length == 1) { } finally {
// Used for testing. if (errors.isNotEmpty) {
flutterPackage = Directory(arguments.single); for (String filePath in errors.keys) {
errors[filePath].forEach(stderr.writeln);
}
stderr.writeln('\nFound ${errors.length} sample code errors.');
}
try {
_tempDir.deleteSync(recursive: true);
} on FileSystemException catch (e) {
stderr.writeln('Failed to delete ${_tempDir.path}: $e');
}
// If we made a snapshot, remove it (so as not to clutter up the tree).
if (_snippetsSnapshotPath != null) {
final File snapshot = File(_snippetsSnapshotPath);
if (snapshot.existsSync()) {
snapshot.deleteSync();
}
}
}
return _exitCode;
}
/// Creates a name for the snippets tool to use for the snippet ID from a
/// filename and starting line number.
String _createNameFromSource(String prefix, String filename, int start) {
String snippetId = path.relative(filename, from: _flutterPackage.path);
snippetId = path.split(snippetId).join('.');
snippetId = path.basenameWithoutExtension(snippetId);
snippetId = '$prefix.$snippetId.$start';
return snippetId;
}
// Precompiles the snippets tool if _snippetsSnapshotPath isn't set yet, and
// runs the precompiled version if it is set.
ProcessResult _runSnippetsScript(List<String> args) {
if (_snippetsSnapshotPath == null) {
_snippetsSnapshotPath = '$_snippetsExecutable.snapshot';
return Process.runSync(
Platform.executable,
<String>[
'--snapshot=$_snippetsSnapshotPath',
'--snapshot-kind=app-jit',
_snippetsExecutable,
]..addAll(args),
workingDirectory: _flutterRoot,
);
} else { } else {
flutterPackage = Directory(path.join(_flutterRoot, 'packages', 'flutter', 'lib')); return Process.runSync(
Platform.executable,
<String>[_snippetsSnapshotPath]..addAll(args),
workingDirectory: _flutterRoot,
);
} }
}
/// Writes out the given [snippet] to an output file in the [_tempDir] and
/// returns the output file.
File _writeSnippet(Snippet snippet) {
// Generate the snippet.
final String snippetId = _createNameFromSource('snippet', snippet.start.filename, snippet.start.line);
final String inputName = '$snippetId.input';
// Now we have a filename like 'lib.src.material.foo_widget.123.dart' for each snippet.
final File inputFile = File(path.join(_tempDir.path, inputName))..createSync(recursive: true);
inputFile.writeAsStringSync(snippet.input.join('\n'));
final File outputFile = File(path.join(_tempDir.path, '$snippetId.dart'));
final List<String> args = <String>[
'--output=${outputFile.absolute.path}',
'--input=${inputFile.absolute.path}',
]..addAll(snippet.args);
print('Generating snippet for ${snippet.start?.filename}:${snippet.start?.line}');
final ProcessResult process = _runSnippetsScript(args);
if (process.exitCode != 0) {
throw 'Unable to create snippet for ${snippet.start.filename}:${snippet.start.line} '
'(using input from ${inputFile.path}):\n${process.stdout}\n${process.stderr}';
}
return outputFile;
}
/// Extracts the samples from the Dart files in [_flutterPackage], writes them
/// to disk, and adds them to the appropriate [sectionMap] or [snippetMap].
void _extractSamples(Map<String, Section> sectionMap, Map<String, Snippet> snippetMap) {
final List<Section> sections = <Section>[]; final List<Section> sections = <Section>[];
int sampleCodeSections = 0; final List<Snippet> snippets = <Snippet>[];
for (FileSystemEntity file in flutterPackage.listSync(recursive: true, followLinks: false)) {
if (file is File && path.extension(file.path) == '.dart') { for (File file in _listDartFiles(_flutterPackage, recursive: true)) {
final List<String> lines = file.readAsLinesSync(); final String relativeFilePath = path.relative(file.path, from: _flutterPackage.path);
final List<String> sampleLines = file.readAsLinesSync();
final List<Section> preambleSections = <Section>[];
bool inPreamble = false; bool inPreamble = false;
bool inSampleSection = false; bool inSampleSection = false;
bool inSnippet = false;
bool inDart = false; bool inDart = false;
bool foundDart = false; bool foundDart = false;
int lineNumber = 0; int lineNumber = 0;
final List<String> block = <String>[]; final List<String> block = <String>[];
List<String> snippetArgs = <String>[];
Line startLine; Line startLine;
for (String line in lines) { for (String line in sampleLines) {
lineNumber += 1; lineNumber += 1;
final String trimmedLine = line.trim(); final String trimmedLine = line.trim();
if (inPreamble) { if (inSnippet) {
if (!trimmedLine.startsWith(_dartDocPrefix)) {
throw '$relativeFilePath:$lineNumber: Snippet section unterminated.';
}
if (_dartDocSampleEndRegex.hasMatch(trimmedLine)) {
snippets.add(
Snippet(
start: startLine,
input: block,
args: snippetArgs,
serial: snippets.length,
),
);
snippetArgs = <String>[];
block.clear();
inSnippet = false;
inSampleSection = false;
} else {
block.add(line.replaceFirst(RegExp(r'\s*/// ?'), ''));
}
} else if (inPreamble) {
if (line.isEmpty) { if (line.isEmpty) {
inPreamble = false; inPreamble = false;
processBlock(startLine, block, sections); preambleSections.add(_processBlock(startLine, block));
block.clear();
} else if (!line.startsWith('// ')) { } else if (!line.startsWith('// ')) {
throw '${file.path}:$lineNumber: Unexpected content in sample code preamble.'; throw '$relativeFilePath:$lineNumber: Unexpected content in sample code preamble.';
} else { } else {
block.add(line.substring(3)); block.add(line.substring(3));
} }
} else if (inSampleSection) { } else if (inSampleSection) {
if (!trimmedLine.startsWith(kDartDocPrefix) || trimmedLine.startsWith('/// ## ')) { if (!trimmedLine.startsWith(_dartDocPrefix) || trimmedLine.startsWith('$_dartDocPrefix ## ')) {
if (inDart) if (inDart) {
throw '${file.path}:$lineNumber: Dart section inexplicably unterminated.'; throw '$relativeFilePath:$lineNumber: Dart section inexplicably unterminated.';
if (!foundDart) }
throw '${file.path}:$lineNumber: No dart block found in sample code section'; if (!foundDart) {
throw '$relativeFilePath:$lineNumber: No dart block found in sample code section';
}
inSampleSection = false; inSampleSection = false;
} else { } else {
if (inDart) { if (inDart) {
if (trimmedLine == '/// ```') { if (_codeBlockEndRegex.hasMatch(trimmedLine)) {
inDart = false; inDart = false;
processBlock(startLine, block, sections); final Section processed = _processBlock(startLine, block);
} else if (trimmedLine == kDartDocPrefix) { if (preambleSections.isEmpty) {
sections.add(processed);
} else {
sections.add(Section.combine(preambleSections
..toList()
..add(processed)));
}
block.clear();
} else if (trimmedLine == _dartDocPrefix) {
block.add(''); block.add('');
} else { } else {
final int index = line.indexOf(kDartDocPrefixWithSpace); final int index = line.indexOf(_dartDocPrefixWithSpace);
if (index < 0) if (index < 0) {
throw '${file.path}:$lineNumber: Dart section inexplicably did not contain "$kDartDocPrefixWithSpace" prefix.'; throw '$relativeFilePath:$lineNumber: Dart section inexplicably did not '
'contain "$_dartDocPrefixWithSpace" prefix.';
}
block.add(line.substring(index + 4)); block.add(line.substring(index + 4));
} }
} else if (trimmedLine == '/// ```dart') { } else if (_codeBlockStartRegex.hasMatch(trimmedLine)) {
assert(block.isEmpty); assert(block.isEmpty);
startLine = Line(file.path, lineNumber + 1, line.indexOf(kDartDocPrefixWithSpace) + kDartDocPrefixWithSpace.length); startLine = Line(
'',
filename: relativeFilePath,
line: lineNumber + 1,
indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length,
);
inDart = true; inDart = true;
foundDart = true; foundDart = true;
} }
} }
} }
if (!inSampleSection) { if (!inSampleSection) {
final Match sampleMatch = _dartDocSampleBeginRegex.firstMatch(trimmedLine);
if (line == '// Examples can assume:') { if (line == '// Examples can assume:') {
assert(block.isEmpty); assert(block.isEmpty);
startLine = Line(file.path, lineNumber + 1, 3); startLine = Line('', filename: relativeFilePath, line: lineNumber + 1, indent: 3);
inPreamble = true; inPreamble = true;
} else if (trimmedLine == '/// ## Sample code' || } else if (trimmedLine == '/// ## Sample code' ||
trimmedLine.startsWith('/// ## Sample code:') || trimmedLine.startsWith('/// ## Sample code:') ||
trimmedLine == '/// ### Sample code' || trimmedLine == '/// ### Sample code' ||
trimmedLine.startsWith('/// ### Sample code:')) { trimmedLine.startsWith('/// ### Sample code:') ||
inSampleSection = true; sampleMatch != null) {
inSnippet = sampleMatch != null ? sampleMatch[1] == 'snippet' : false;
if (inSnippet) {
startLine = Line(
'',
filename: relativeFilePath,
line: lineNumber + 1,
indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length,
);
if (sampleMatch[2] != null) {
// There are arguments to the snippet tool to keep track of.
snippetArgs = _splitUpQuotedArgs(sampleMatch[2]).toList();
} else {
snippetArgs = <String>[];
}
}
inSampleSection = !inSnippet;
foundDart = false; foundDart = false;
sampleCodeSections += 1;
} }
} }
} }
} }
print('Found ${sections.length} sample code sections.');
for (Section section in sections) {
sectionMap[_writeSection(section).path] = section;
} }
buffer.add('// generated code'); for (Snippet snippet in snippets) {
buffer.add('import \'dart:async\';'); final File snippetFile = _writeSnippet(snippet);
buffer.add('import \'dart:convert\';'); snippet.contents = snippetFile.readAsLinesSync();
buffer.add('import \'dart:math\' as math;'); snippetMap[snippetFile.absolute.path] = snippet;
buffer.add('import \'dart:typed_data\';');
buffer.add('import \'dart:ui\' as ui;');
buffer.add('import \'package:flutter_test/flutter_test.dart\';');
for (FileSystemEntity file in flutterPackage.listSync(recursive: false, followLinks: false)) {
if (file is File && path.extension(file.path) == '.dart') {
buffer.add('');
buffer.add('// ${file.path}');
buffer.add('import \'package:flutter/${path.basename(file.path)}\';');
} }
} }
buffer.add('');
final List<Line> lines = List<Line>.filled(buffer.length, null, growable: true); /// Helper to process arguments given as a (possibly quoted) string.
for (Section section in sections) { ///
buffer.addAll(section.strings); /// First, this will split the given [argsAsString] into separate arguments,
lines.addAll(section.lines); /// taking any quoting (either ' or " are accepted) into account, including
/// handling backslash-escaped quotes.
///
/// Then, it will prepend "--" to any args that start with an identifier
/// followed by an equals sign, allowing the argument parser to treat any
/// "foo=bar" argument as "--foo=bar" (which is a dartdoc-ism).
Iterable<String> _splitUpQuotedArgs(String argsAsString) {
// Regexp to take care of splitting arguments, and handling the quotes
// around arguments, if any.
//
// Match group 1 is the "foo=" (or "--foo=") part of the option, if any.
// Match group 2 contains the quote character used (which is discarded).
// Match group 3 is a quoted arg, if any, without the quotes.
// Match group 4 is the unquoted arg, if any.
final RegExp argMatcher = RegExp(r'([a-zA-Z\-_0-9]+=)?' // option name
r'(?:' // Start a new non-capture group for the two possibilities.
r'''(["'])((?:\\{2})*|(?:.*?[^\\](?:\\{2})*))\2|''' // with quotes.
r'([^ ]+))'); // without quotes.
final Iterable<Match> matches = argMatcher.allMatches(argsAsString);
// Remove quotes around args, and if convertToArgs is true, then for any
// args that look like assignments (start with valid option names followed
// by an equals sign), add a "--" in front so that they parse as options.
return matches.map<String>((Match match) {
String option = '';
if (match[1] != null && !match[1].startsWith('-')) {
option = '--';
}
if (match[2] != null) {
// This arg has quotes, so strip them.
return '$option${match[1] ?? ''}${match[3] ?? ''}${match[4] ?? ''}';
}
return '$option${match[0]}';
});
} }
assert(buffer.length == lines.length);
mainDart.writeAsStringSync(buffer.join('\n')); /// Creates the configuration files necessary for the analyzer to consider
/// the temporary director a package, and sets which lint rules to enforce.
void _createConfigurationFiles(Directory directory) {
final File pubSpec = File(path.join(directory.path, 'pubspec.yaml'))..createSync(recursive: true);
final File analysisOptions = File(path.join(directory.path, 'analysis_options.yaml'))..createSync(recursive: true);
pubSpec.writeAsStringSync(''' pubSpec.writeAsStringSync('''
name: analyze_sample_code name: analyze_sample_code
dependencies: dependencies:
...@@ -220,145 +443,391 @@ linter: ...@@ -220,145 +443,391 @@ linter:
- unnecessary_const - unnecessary_const
- unnecessary_new - unnecessary_new
'''); ''');
print('Found $sampleCodeSections sample code sections.'); }
final Process process = await Process.start(
/// Writes out a sample section to the disk and returns the file.
File _writeSection(Section section) {
final String sectionId = _createNameFromSource('sample', section.start.filename, section.start.line);
final File outputFile = File(path.join(_tempDir.path, '$sectionId.dart'))..createSync(recursive: true);
final List<Line> mainContents = headers.toList();
mainContents.add(const Line(''));
mainContents.add(Line('// From: ${section.start.filename}:${section.start.line}'));
mainContents.addAll(section.code);
outputFile.writeAsStringSync(mainContents.map<String>((Line line) => line.code).join('\n'));
return outputFile;
}
/// Invokes the analyzer on the given [directory] and returns the stdout.
List<String> _runAnalyzer(Directory directory) {
print('Starting analysis of samples.');
_createConfigurationFiles(directory);
final ProcessResult result = Process.runSync(
_flutter, _flutter,
<String>['--no-wrap', 'analyze', '--no-preamble', '--no-congratulate', mainDart.parent.path], <String>['--no-wrap', 'analyze', '--no-preamble', '--no-congratulate', '.'],
workingDirectory: tempDir.path, workingDirectory: directory.absolute.path,
); );
final List<String> errors = <String>[]; final List<String> stderr = result.stderr.toString().trim().split('\n');
errors.addAll(await process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).toList()); final List<String> stdout = result.stdout.toString().trim().split('\n');
errors.add(null); // Check out the stderr to see if the analyzer had it's own issues.
errors.addAll(await process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).toList()); if (stderr.isNotEmpty && (stderr.first.contains(' issues found. (ran in ') || stderr.first.contains(' issue found. (ran in '))) {
// top is stderr // The "23 issues found" message goes onto stderr, which is concatenated first.
if (errors.isNotEmpty && (errors.first.contains(' issues found. (ran in ') || errors.first.contains(' issue found. (ran in '))) { stderr.removeAt(0);
errors.removeAt(0); // the "23 issues found" message goes onto stderr, which is concatenated first // If there's an "issues found" message, we put a blank line on stdout before it.
if (errors.isNotEmpty && errors.last.isEmpty) if (stderr.isNotEmpty && stderr.last.isEmpty) {
errors.removeLast(); // if there's an "issues found" message, we put a blank line on stdout before it stderr.removeLast();
} }
// null separates stderr from stdout }
if (errors.first != null) if (stderr.isNotEmpty) {
throw 'cannot analyze dartdocs; unexpected error output: $errors'; throw 'Cannot analyze dartdocs; unexpected error output:\n$stderr';
errors.removeAt(0); }
// rest is stdout if (stdout.isNotEmpty && stdout.first == 'Building flutter tool...') {
if (errors.isNotEmpty && errors.first == 'Building flutter tool...') stdout.removeAt(0);
errors.removeAt(0); }
if (errors.isNotEmpty && errors.first.startsWith('Running "flutter packages get" in ')) if (stdout.isNotEmpty && stdout.first.startsWith('Running "flutter packages get" in ')) {
errors.removeAt(0); stdout.removeAt(0);
int errorCount = 0; }
_exitCode = result.exitCode;
return stdout;
}
/// Starts the analysis phase of checking the samples by invoking the analyzer
/// and parsing its output to create a map of filename to [AnalysisError]s.
Map<String, List<AnalysisError>> _analyze(
Directory directory,
Map<String, Section> sections,
Map<String, Snippet> snippets,
) {
final List<String> errors = _runAnalyzer(directory);
final Map<String, List<AnalysisError>> analysisErrors = <String, List<AnalysisError>>{};
void addAnalysisError(File file, AnalysisError error) {
if (analysisErrors.containsKey(file.path)) {
analysisErrors[file.path].add(error);
} else {
analysisErrors[file.path] = <AnalysisError>[error];
}
}
final String kBullet = Platform.isWindows ? ' - ' : ' • '; final String kBullet = Platform.isWindows ? ' - ' : ' • ';
final RegExp errorPattern = RegExp('^ +([a-z]+)$kBullet(.+)$kBullet(.+):([0-9]+):([0-9]+)$kBullet([-a-z_]+)\$', caseSensitive: false); // RegExp to match an error output line of the analyzer.
final RegExp errorPattern = RegExp(
'^ +([a-z]+)$kBullet(.+)$kBullet(.+):([0-9]+):([0-9]+)$kBullet([-a-z_]+)\$',
caseSensitive: false,
);
bool unknownAnalyzerErrors = false;
final int headerLength = headers.length + 2;
for (String error in errors) { for (String error in errors) {
final Match parts = errorPattern.matchAsPrefix(error); final Match parts = errorPattern.matchAsPrefix(error);
if (parts != null) { if (parts != null) {
final String message = parts[2]; final String message = parts[2];
final String file = parts[3]; final File file = File(path.join(_tempDir.path, parts[3]));
final List<String> fileContents = file.readAsLinesSync();
final bool isSnippet = path.basename(file.path).startsWith('snippet.');
final bool isSample = path.basename(file.path).startsWith('sample.');
final String line = parts[4]; final String line = parts[4];
final String column = parts[5]; final String column = parts[5];
final String errorCode = parts[6]; final String errorCode = parts[6];
final int lineNumber = int.parse(line, radix: 10); final int lineNumber = int.parse(line, radix: 10) - (isSample ? headerLength : 0);
final int columnNumber = int.parse(column, radix: 10); final int columnNumber = int.parse(column, radix: 10);
if (file != 'main.dart') { if (lineNumber < 0 && errorCode == 'unused_import') {
keepMain = true; // We don't care about unused imports.
throw 'cannot analyze dartdocs; analysis errors exist in $file: $error'; continue;
} }
if (lineNumber < 1 || lineNumber > lines.length) {
keepMain = true; // For when errors occur outside of the things we're trying to analyze.
throw 'failed to parse error message (read line number as $lineNumber; total number of lines is ${lines.length}): $error'; if (!isSnippet && !isSample) {
addAnalysisError(
file,
AnalysisError(
lineNumber,
columnNumber,
message,
errorCode,
Line(
'',
filename: file.path,
line: lineNumber,
),
),
);
throw 'Cannot analyze dartdocs; analysis errors exist in ${file.path}: $error';
} }
final Line actualLine = lines[lineNumber - 1];
if (errorCode == 'unused_element' || errorCode == 'unused_local_variable') { if (errorCode == 'unused_element' || errorCode == 'unused_local_variable') {
// We don't really care if sample code isn't used! // We don't really care if sample code isn't used!
} else if (actualLine == null) { continue;
if (errorCode == 'missing_identifier' && lineNumber > 1 && buffer[lineNumber - 2].endsWith(',')) { }
final Line actualLine = lines[lineNumber - 2];
print('${actualLine.toString(buffer[lineNumber - 2].length - 1)}: unexpected comma at end of sample code'); if (isSnippet) {
errorCount += 1; addAnalysisError(
file,
AnalysisError(
lineNumber,
columnNumber,
message,
errorCode,
null,
snippet: snippets[file.path],
),
);
} else { } else {
print('${mainDart.path}:${lineNumber - 1}:$columnNumber: $message'); if (lineNumber < 1 || lineNumber > fileContents.length) {
keepMain = true; addAnalysisError(
errorCount += 1; file,
AnalysisError(
lineNumber,
columnNumber,
message,
errorCode,
Line('', filename: file.path, line: lineNumber),
),
);
throw 'Failed to parse error message (read line number as $lineNumber; '
'total number of lines is ${fileContents.length}): $error';
}
final Section actualSection = sections[file.path];
final Line actualLine = actualSection.code[lineNumber - 1];
if (actualLine.filename == null) {
if (errorCode == 'missing_identifier' && lineNumber > 1) {
if (fileContents[lineNumber - 2].endsWith(',')) {
final Line actualLine = sections[file.path].code[lineNumber - 2];
addAnalysisError(
file,
AnalysisError(
actualLine.line,
actualLine.indent + fileContents[lineNumber - 2].length - 1,
'Unexpected comma at end of sample code.',
errorCode,
actualLine,
),
);
} }
} else { } else {
print('${actualLine.toString(columnNumber)}: $message ($errorCode)'); addAnalysisError(
errorCount += 1; file,
AnalysisError(
lineNumber - 1,
columnNumber,
message,
errorCode,
actualLine,
),
);
} }
} else { } else {
print('?? $error'); addAnalysisError(
keepMain = true; file,
errorCount += 1; AnalysisError(
actualLine.line,
actualLine.indent + columnNumber,
message,
errorCode,
actualLine,
),
);
} }
} }
exitCode = await process.exitCode;
if (exitCode == 1 && errorCount == 0)
exitCode = 0;
if (exitCode == 0)
print('No errors!');
} finally {
if (keepMain) {
print('Kept ${tempDir.path} because it had errors (see above).');
print('-------8<-------');
int number = 1;
for (String line in buffer) {
print('${number.toString().padLeft(6, " ")}: $line');
number += 1;
}
print('-------8<-------');
} else { } else {
try { stderr.writeln('Analyzer output: $error');
tempDir.deleteSync(recursive: true); unknownAnalyzerErrors = true;
} on FileSystemException catch (e) {
print('Failed to delete ${tempDir.path}: $e');
} }
} }
if (_exitCode == 1 && analysisErrors.isEmpty && !unknownAnalyzerErrors) {
_exitCode = 0;
}
if (_exitCode == 0) {
print('No analysis errors in samples!');
assert(analysisErrors.isEmpty);
}
return analysisErrors;
} }
exit(exitCode);
}
final RegExp _constructorRegExp = RegExp(r'[A-Z][a-zA-Z0-9<>.]*\(');
int _expressionId = 0;
void processBlock(Line line, List<String> block, List<Section> sections) { /// Process one block of sample code (the part inside of "```" markers).
if (block.isEmpty) /// Splits any sections denoted by "// ..." into separate blocks to be
/// processed separately. Uses a primitive heuristic to make sample blocks
/// into valid Dart code.
Section _processBlock(Line line, List<String> block) {
if (block.isEmpty) {
throw '$line: Empty ```dart block in sample code.'; throw '$line: Empty ```dart block in sample code.';
}
if (block.first.startsWith('new ') || block.first.startsWith('const ') || block.first.startsWith(_constructorRegExp)) { if (block.first.startsWith('new ') || block.first.startsWith('const ') || block.first.startsWith(_constructorRegExp)) {
_expressionId += 1; _expressionId += 1;
sections.add(Section(line, 'dynamic expression$_expressionId = ', block.toList(), ';')); return Section.surround(line, 'dynamic expression$_expressionId = ', block.toList(), ';');
} else if (block.first.startsWith('await ')) { } else if (block.first.startsWith('await ')) {
_expressionId += 1; _expressionId += 1;
sections.add(Section(line, 'Future<void> expression$_expressionId() async { ', block.toList(), ' }')); return Section.surround(line, 'Future<void> expression$_expressionId() async { ', block.toList(), ' }');
} else if (block.first.startsWith('class ') || block.first.startsWith('enum ')) { } else if (block.first.startsWith('class ') || block.first.startsWith('enum ')) {
sections.add(Section(line, null, block.toList(), null)); return Section.fromStrings(line, block.toList());
} else if ((block.first.startsWith('_') || block.first.startsWith('final ')) && block.first.contains(' = ')) { } else if ((block.first.startsWith('_') || block.first.startsWith('final ')) && block.first.contains(' = ')) {
_expressionId += 1; _expressionId += 1;
sections.add(Section(line, 'void expression$_expressionId() { ', block.toList(), ' }')); return Section.surround(line, 'void expression$_expressionId() { ', block.toList(), ' }');
} else { } else {
final List<String> buffer = <String>[]; final List<String> buffer = <String>[];
int subblocks = 0; int subblocks = 0;
Line subline; Line subline;
final List<Section> subsections = <Section>[];
for (int index = 0; index < block.length; index += 1) { for (int index = 0; index < block.length; index += 1) {
// Each section of the dart code that is either split by a blank line, or with '// ...' is
// treated as a separate code block.
if (block[index] == '' || block[index] == '// ...') { if (block[index] == '' || block[index] == '// ...') {
if (subline == null) if (subline == null)
throw '${line + index}: Unexpected blank line or "// ..." line near start of subblock in sample code.'; throw '${Line('', filename: line.filename, line: line.line + index, indent: line.indent)}: '
'Unexpected blank line or "// ..." line near start of subblock in sample code.';
subblocks += 1; subblocks += 1;
processBlock(subline, buffer, sections); subsections.add(_processBlock(subline, buffer));
buffer.clear();
assert(buffer.isEmpty); assert(buffer.isEmpty);
subline = null; subline = null;
} else if (block[index].startsWith('// ')) { } else if (block[index].startsWith('// ')) {
if (buffer.length > 1) // don't include leading comments if (buffer.length > 1) // don't include leading comments
buffer.add('/${block[index]}'); // so that it doesn't start with "// " and get caught in this again buffer.add('/${block[index]}'); // so that it doesn't start with "// " and get caught in this again
} else { } else {
subline ??= line + index; subline ??= Line(
block[index],
filename: line.filename,
line: line.line + index,
indent: line.indent,
);
buffer.add(block[index]); buffer.add(block[index]);
} }
} }
if (subblocks > 0) { if (subblocks > 0) {
if (subline != null) if (subline != null) {
processBlock(subline, buffer, sections); subsections.add(_processBlock(subline, buffer));
}
// Combine all of the subsections into one section, now that they've been processed.
return Section.combine(subsections);
} else { } else {
sections.add(Section(line, null, block.toList(), null)); return Section.fromStrings(line, block.toList());
}
}
}
}
/// A class to represent a line of input code.
class Line {
const Line(this.code, {this.filename, this.line, this.indent});
final String filename;
final int line;
final int indent;
final String code;
String toStringWithColumn(int column) {
if (column != null) {
return '$filename:$line:${column + indent}: $code';
}
return toString();
}
@override
String toString() => '$filename:$line: $code';
}
/// A class to represent a section of sample code, either marked by
/// "/// ## Sample code" in the comment, or by "{@tool sample}...{@end-tool}".
class Section {
const Section(this.code);
factory Section.combine(List<Section> sections) {
final List<Line> code = <Line>[];
for (Section section in sections) {
code.addAll(section.code);
}
return Section(code);
}
factory Section.fromStrings(Line firstLine, List<String> code) {
final List<Line> codeLines = <Line>[];
for (int i = 0; i < code.length; ++i) {
codeLines.add(
Line(
code[i],
filename: firstLine.filename,
line: firstLine.line + i,
indent: firstLine.indent,
),
);
}
return Section(codeLines);
}
factory Section.surround(Line firstLine, String prefix, List<String> code, String postfix) {
assert(prefix != null);
assert(postfix != null);
final List<Line> codeLines = <Line>[];
for (int i = 0; i < code.length; ++i) {
codeLines.add(
Line(
code[i],
filename: firstLine.filename,
line: firstLine.line + i,
indent: firstLine.indent,
),
);
}
return Section(<Line>[Line(prefix)]
..addAll(codeLines)
..add(Line(postfix)));
}
Line get start => code.firstWhere((Line line) => line.filename != null);
final List<Line> code;
}
/// A class to represent a snippet in the dartdoc comments, marked by
/// "{@tool snippet ...}...{@end-tool}". Snippets are processed separately from
/// regular samples, because they must be injected into templates in order to be
/// analyzed.
class Snippet {
Snippet({this.start, List<String> input, List<String> args, this.serial}) {
this.input = <String>[]..addAll(input);
this.args = <String>[]..addAll(args);
}
final Line start;
final int serial;
List<String> input;
List<String> args;
List<String> contents;
@override
String toString() {
final StringBuffer buf = StringBuffer('snippet ${args.join(' ')}\n');
int count = start.line;
for (String line in input) {
buf.writeln(' ${count.toString().padLeft(4, ' ')}: $line');
count++;
}
return buf.toString();
}
}
/// A class representing an analysis error along with the context of the error.
///
/// Changes how it converts to a string based on the source of the error.
class AnalysisError {
const AnalysisError(
this.line,
this.column,
this.message,
this.errorCode,
this.source, {
this.snippet,
});
final int line;
final int column;
final String message;
final String errorCode;
final Line source;
final Snippet snippet;
@override
String toString() {
if (source != null) {
return '${source.toStringWithColumn(column)}\n>>> $message ($errorCode)';
} else if (snippet != null) {
return 'In snippet starting at '
'${snippet.start.filename}:${snippet.start.line}:${snippet.contents[line - 1]}\n'
'>>> $message ($errorCode)';
} else {
return '<source unknown>:$line:$column\n>>> $message ($errorCode)';
} }
} }
block.clear();
} }
...@@ -2,68 +2,29 @@ ...@@ -2,68 +2,29 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'common.dart'; import 'common.dart';
void main() { void main() {
test('analyze-sample-code', () async { test('analyze-sample-code', () {
final Process process = await Process.start( final ProcessResult process = Process.runSync(
'../../bin/cache/dart-sdk/bin/dart', '../../bin/cache/dart-sdk/bin/dart',
<String>['analyze-sample-code.dart', 'test/analyze-sample-code-test-input'], <String>['analyze-sample-code.dart', 'test/analyze-sample-code-test-input'],
); );
final List<String> stdout = await process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).toList(); final List<String> stdoutLines = process.stdout.toString().split('\n');
final List<String> stderr = await process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).toList(); final List<String> stderrLines = process.stderr.toString().split('\n')
final Match line = RegExp(r'^(.+)/main\.dart:[0-9]+:[0-9]+: .+$').matchAsPrefix(stdout[1]); ..removeWhere((String line) => line.startsWith('Analyzer output:'));
expect(line, isNot(isNull)); expect(process.exitCode, isNot(equals(0)));
final String directory = line.group(1); expect(stderrLines, <String>[
Directory(directory).deleteSync(recursive: true); 'known_broken_documentation.dart:27:9: new Opacity(',
expect(await process.exitCode, 1); '>>> Unnecessary new keyword (unnecessary_new)',
expect(stderr, isEmpty); 'known_broken_documentation.dart:39:9: new Opacity(',
expect(stdout, <String>[ '>>> Unnecessary new keyword (unnecessary_new)',
'Found 2 sample code sections.', '',
"$directory/main.dart:1:8: Unused import: 'dart:async'", 'Found 1 sample code errors.',
"$directory/main.dart:2:8: Unused import: 'dart:convert'", '',
"$directory/main.dart:3:8: Unused import: 'dart:math'",
"$directory/main.dart:4:8: Unused import: 'dart:typed_data'",
"$directory/main.dart:5:8: Unused import: 'dart:ui'",
"$directory/main.dart:6:8: Unused import: 'package:flutter_test/flutter_test.dart'",
"$directory/main.dart:9:8: Target of URI doesn't exist: 'package:flutter/known_broken_documentation.dart'",
'test/analyze-sample-code-test-input/known_broken_documentation.dart:27:5: Unnecessary new keyword (unnecessary_new)',
"test/analyze-sample-code-test-input/known_broken_documentation.dart:27:9: Undefined class 'Opacity' (undefined_class)",
"test/analyze-sample-code-test-input/known_broken_documentation.dart:29:20: Undefined class 'Text' (undefined_class)",
'test/analyze-sample-code-test-input/known_broken_documentation.dart:39:5: Unnecessary new keyword (unnecessary_new)',
"test/analyze-sample-code-test-input/known_broken_documentation.dart:39:9: Undefined class 'Opacity' (undefined_class)",
"test/analyze-sample-code-test-input/known_broken_documentation.dart:41:20: Undefined class 'Text' (undefined_class)",
'test/analyze-sample-code-test-input/known_broken_documentation.dart:42:5: unexpected comma at end of sample code',
'Kept $directory because it had errors (see above).',
'-------8<-------',
' 1: // generated code',
" 2: import 'dart:async';",
" 3: import 'dart:convert';",
" 4: import 'dart:math' as math;",
" 5: import 'dart:typed_data';",
" 6: import 'dart:ui' as ui;",
" 7: import 'package:flutter_test/flutter_test.dart';",
' 8: ',
' 9: // test/analyze-sample-code-test-input/known_broken_documentation.dart',
" 10: import 'package:flutter/known_broken_documentation.dart';",
' 11: ',
' 12: bool _visible = true;',
' 13: dynamic expression1 = ',
' 14: new Opacity(',
' 15: opacity: _visible ? 1.0 : 0.0,',
" 16: child: const Text('Poor wandering ones!'),",
' 17: )',
' 18: ;',
' 19: dynamic expression2 = ',
' 20: new Opacity(',
' 21: opacity: _visible ? 1.0 : 0.0,',
" 22: child: const Text('Poor wandering ones!'),",
' 23: ),',
' 24: ;',
'-------8<-------',
]); ]);
}, skip: !Platform.isLinux); expect(stdoutLines, <String>['Found 2 sample code sections.', 'Starting analysis of samples.', '']);
}, skip: Platform.isWindows);
} }
...@@ -13,20 +13,20 @@ class MyApp extends StatelessWidget { ...@@ -13,20 +13,20 @@ class MyApp extends StatelessWidget {
theme: new ThemeData( theme: new ThemeData(
primarySwatch: Colors.blue, primarySwatch: Colors.blue,
), ),
home: new MyHomePage(title: '{{id}} Sample'), home: new MyStatefulWidget(),
); );
} }
} }
{{code-preamble}} {{code-preamble}}
class MyHomePage extends StatelessWidget { class MyStatefulWidget extends StatefulWidget {
MyHomePage({Key key}) : super(key: key); MyStatefulWidget({Key key}) : super(key: key);
@override @override
_MyHomePageState createState() => new _MyHomePageState(); _MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
} }
class _MyHomePageState extends State<MyHomePage> { class _MyStatefulWidgetState extends State<MyStatefulWidget> {
{{code}} {{code}}
} }
{{description}}
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Code Sample for {{id}}',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyStatelessWidget(),
);
}
}
{{code-preamble}}
class MyStatelessWidget extends StatelessWidget {
MyStatelessWidget({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return {{code}}
}
}
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:io' hide Platform; import 'dart:io' hide Platform;
import 'package:args/args.dart'; import 'package:args/args.dart';
import 'package:path/path.dart' as path;
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import 'configuration.dart'; import 'configuration.dart';
...@@ -16,6 +17,7 @@ const String _kLibraryOption = 'library'; ...@@ -16,6 +17,7 @@ const String _kLibraryOption = 'library';
const String _kPackageOption = 'package'; const String _kPackageOption = 'package';
const String _kTemplateOption = 'template'; const String _kTemplateOption = 'template';
const String _kTypeOption = 'type'; const String _kTypeOption = 'type';
const String _kOutputOption = 'output';
/// Generates snippet dartdoc output for a given input, and creates any sample /// Generates snippet dartdoc output for a given input, and creates any sample
/// applications needed by the snippet. /// applications needed by the snippet.
...@@ -44,6 +46,13 @@ void main(List<String> argList) { ...@@ -44,6 +46,13 @@ void main(List<String> argList) {
defaultsTo: null, defaultsTo: null,
help: 'The name of the template to inject the code into.', help: 'The name of the template to inject the code into.',
); );
parser.addOption(
_kOutputOption,
defaultsTo: null,
help: 'The output path for the generated snippet application. Overrides '
'the naming generated by the --package/--library/--element arguments. '
'The basename of this argument is used as the ID',
);
parser.addOption( parser.addOption(
_kInputOption, _kInputOption,
defaultsTo: environment['INPUT'], defaultsTo: environment['INPUT'],
...@@ -93,6 +102,9 @@ void main(List<String> argList) { ...@@ -93,6 +102,9 @@ void main(List<String> argList) {
} }
final List<String> id = <String>[]; final List<String> id = <String>[];
if (args[_kOutputOption] != null) {
id.add(path.basename(path.basenameWithoutExtension(args[_kOutputOption])));
} else {
if (args[_kPackageOption] != null && if (args[_kPackageOption] != null &&
args[_kPackageOption].isNotEmpty && args[_kPackageOption].isNotEmpty &&
args[_kPackageOption] != 'flutter') { args[_kPackageOption] != 'flutter') {
...@@ -104,12 +116,12 @@ void main(List<String> argList) { ...@@ -104,12 +116,12 @@ void main(List<String> argList) {
if (args[_kElementOption] != null && args[_kElementOption].isNotEmpty) { if (args[_kElementOption] != null && args[_kElementOption].isNotEmpty) {
id.add(args[_kElementOption]); id.add(args[_kElementOption]);
} }
if (id.isEmpty) { if (id.isEmpty) {
errorExit('Unable to determine ID. At least one of --$_kPackageOption, ' errorExit('Unable to determine ID. At least one of --$_kPackageOption, '
'--$_kLibraryOption, --$_kElementOption, or the environment variables ' '--$_kLibraryOption, --$_kElementOption, or the environment variables '
'PACKAGE_NAME, LIBRARY_NAME, or ELEMENT_NAME must be non-empty.'); 'PACKAGE_NAME, LIBRARY_NAME, or ELEMENT_NAME must be non-empty.');
} }
}
final SnippetGenerator generator = SnippetGenerator(); final SnippetGenerator generator = SnippetGenerator();
stdout.write(generator.generate( stdout.write(generator.generate(
...@@ -117,6 +129,7 @@ void main(List<String> argList) { ...@@ -117,6 +129,7 @@ void main(List<String> argList) {
snippetType, snippetType,
template: template, template: template,
id: id.join('.'), id: id.join('.'),
output: args[_kOutputOption] != null ? File(args[_kOutputOption]) : null,
)); ));
exit(0); exit(0);
} }
...@@ -55,9 +55,7 @@ class SnippetGenerator { ...@@ -55,9 +55,7 @@ class SnippetGenerator {
/// "description" injection into a comment. Only used for /// "description" injection into a comment. Only used for
/// [SnippetType.application] snippets. /// [SnippetType.application] snippets.
String interpolateTemplate(List<_ComponentTuple> injections, String template) { String interpolateTemplate(List<_ComponentTuple> injections, String template) {
final String injectionMatches = final RegExp moustacheRegExp = RegExp('{{([^}]+)}}');
injections.map<String>((_ComponentTuple tuple) => RegExp.escape(tuple.name)).join('|');
final RegExp moustacheRegExp = RegExp('{{($injectionMatches)}}');
return template.replaceAllMapped(moustacheRegExp, (Match match) { return template.replaceAllMapped(moustacheRegExp, (Match match) {
if (match[1] == 'description') { if (match[1] == 'description') {
// Place the description into a comment. // Place the description into a comment.
...@@ -77,9 +75,13 @@ class SnippetGenerator { ...@@ -77,9 +75,13 @@ class SnippetGenerator {
} }
return description.join('\n').trim(); return description.join('\n').trim();
} else { } else {
// If the match isn't found in the injections, then just remove the
// moustache reference, since we want to allow the sections to be
// "optional" in the input: users shouldn't be forced to add an empty
// "```dart preamble" section if that section would be empty.
return injections return injections
.firstWhere((_ComponentTuple tuple) => tuple.name == match[1]) .firstWhere((_ComponentTuple tuple) => tuple.name == match[1], orElse: () => null)
.mergedContent; ?.mergedContent ?? '';
} }
}).trim(); }).trim();
} }
...@@ -103,17 +105,11 @@ class SnippetGenerator { ...@@ -103,17 +105,11 @@ class SnippetGenerator {
if (result.length > 3) { if (result.length > 3) {
result.removeRange(result.length - 3, result.length); result.removeRange(result.length - 3, result.length);
} }
String formattedCode;
try {
formattedCode = formatter.format(result.join('\n'));
} on FormatterException catch (exception) {
errorExit('Unable to format snippet code: $exception');
}
final Map<String, String> substitutions = <String, String>{ final Map<String, String> substitutions = <String, String>{
'description': injections 'description': injections
.firstWhere((_ComponentTuple tuple) => tuple.name == 'description') .firstWhere((_ComponentTuple tuple) => tuple.name == 'description')
.mergedContent, .mergedContent,
'code': formattedCode, 'code': result.join('\n'),
}..addAll(type == SnippetType.application }..addAll(type == SnippetType.application
? <String, String>{ ? <String, String>{
'id': 'id':
...@@ -182,7 +178,7 @@ class SnippetGenerator { ...@@ -182,7 +178,7 @@ class SnippetGenerator {
/// The [id] is a string ID to use for the output file, and to tell the user /// The [id] is a string ID to use for the output file, and to tell the user
/// about in the `flutter create` hint. It must not be null if the [type] is /// about in the `flutter create` hint. It must not be null if the [type] is
/// [SnippetType.application]. /// [SnippetType.application].
String generate(File input, SnippetType type, {String template, String id}) { String generate(File input, SnippetType type, {String template, String id, File output}) {
assert(template != null || type != SnippetType.application); assert(template != null || type != SnippetType.application);
assert(id != null || type != SnippetType.application); assert(id != null || type != SnippetType.application);
assert(input != null); assert(input != null);
...@@ -207,11 +203,14 @@ class SnippetGenerator { ...@@ -207,11 +203,14 @@ class SnippetGenerator {
try { try {
app = formatter.format(app); app = formatter.format(app);
} on FormatterException catch (exception) { } on FormatterException catch (exception) {
stderr.write('Code to format:\n$app\n');
errorExit('Unable to format snippet app template: $exception'); errorExit('Unable to format snippet app template: $exception');
} }
snippetData.add(_ComponentTuple('app', app.split('\n'))); snippetData.add(_ComponentTuple('app', app.split('\n')));
getOutputFile(id).writeAsStringSync(app); final File outputFile = output ?? getOutputFile(id);
stderr.writeln('Writing to ${outputFile.absolute.path}');
outputFile.writeAsStringSync(app);
break; break;
case SnippetType.sample: case SnippetType.sample:
break; break;
......
...@@ -8,6 +8,9 @@ import 'package:flutter/foundation.dart'; ...@@ -8,6 +8,9 @@ import 'package:flutter/foundation.dart';
import 'tween.dart'; import 'tween.dart';
// Examples can assume:
// AnimationController _controller;
/// The status of an animation /// The status of an animation
enum AnimationStatus { enum AnimationStatus {
/// The animation is stopped at the beginning /// The animation is stopped at the beginning
......
...@@ -19,6 +19,7 @@ export 'package:flutter/scheduler.dart' show TickerFuture, TickerCanceled; ...@@ -19,6 +19,7 @@ export 'package:flutter/scheduler.dart' show TickerFuture, TickerCanceled;
// Examples can assume: // Examples can assume:
// AnimationController _controller, fadeAnimationController, sizeAnimationController; // AnimationController _controller, fadeAnimationController, sizeAnimationController;
// bool dismissed; // bool dismissed;
// void setState(VoidCallback fn) { }
/// The direction in which an animation is running. /// The direction in which an animation is running.
enum _AnimationDirection { enum _AnimationDirection {
......
...@@ -11,6 +11,9 @@ import 'animation.dart'; ...@@ -11,6 +11,9 @@ import 'animation.dart';
import 'curves.dart'; import 'curves.dart';
import 'listener_helpers.dart'; import 'listener_helpers.dart';
// Examples can assume:
// AnimationController controller;
class _AlwaysCompleteAnimation extends Animation<double> { class _AlwaysCompleteAnimation extends Animation<double> {
const _AlwaysCompleteAnimation(); const _AlwaysCompleteAnimation();
......
...@@ -12,6 +12,7 @@ import 'curves.dart'; ...@@ -12,6 +12,7 @@ import 'curves.dart';
// Examples can assume: // Examples can assume:
// Animation<Offset> _animation; // Animation<Offset> _animation;
// AnimationController _controller;
/// An object that can produce a value of type `T` given an [Animation<double>] /// An object that can produce a value of type `T` given an [Animation<double>]
/// as input. /// as input.
...@@ -413,7 +414,7 @@ class ConstantTween<T> extends Tween<T> { ...@@ -413,7 +414,7 @@ class ConstantTween<T> extends Tween<T> {
/// animation produced by an [AnimationController] `controller`: /// animation produced by an [AnimationController] `controller`:
/// ///
/// ```dart /// ```dart
/// final Animation<double> animation = controller.drive( /// final Animation<double> animation = _controller.drive(
/// CurveTween(curve: Curves.ease), /// CurveTween(curve: Curves.ease),
/// ); /// );
/// ``` /// ```
......
...@@ -14,6 +14,7 @@ import 'thumb_painter.dart'; ...@@ -14,6 +14,7 @@ import 'thumb_painter.dart';
// Examples can assume: // Examples can assume:
// int _cupertinoSliderValue = 1; // int _cupertinoSliderValue = 1;
// void setState(VoidCallback fn) { }
/// An iOS-style slider. /// An iOS-style slider.
/// ///
......
...@@ -13,6 +13,10 @@ import 'package:flutter/widgets.dart'; ...@@ -13,6 +13,10 @@ import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'thumb_painter.dart'; import 'thumb_painter.dart';
// Examples can assume:
// bool _lights;
// void setState(VoidCallback fn) { }
/// An iOS-style switch. /// An iOS-style switch.
/// ///
/// Used to toggle the on/off state of a single setting. /// Used to toggle the on/off state of a single setting.
......
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// Examples can assume:
// class Cat { }
/// A category with which to annotate a class, for documentation /// A category with which to annotate a class, for documentation
/// purposes. /// purposes.
/// ///
......
...@@ -6,6 +6,11 @@ import 'package:meta/meta.dart'; ...@@ -6,6 +6,11 @@ import 'package:meta/meta.dart';
import 'print.dart'; import 'print.dart';
// Examples can assume:
// int rows, columns;
// String _name;
// bool inherit;
/// The various priority levels used to filter which diagnostics are shown and /// The various priority levels used to filter which diagnostics are shown and
/// omitted. /// omitted.
/// ///
......
...@@ -219,7 +219,9 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -219,7 +219,9 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// For less common operations, consider using a [PopupMenuButton] as the /// For less common operations, consider using a [PopupMenuButton] as the
/// last action. /// last action.
/// ///
/// ## Sample code /// {@tool snippet --template=stateless_widget}
///
/// This sample shows adding an action to an [AppBar] that opens a shopping cart.
/// ///
/// ```dart /// ```dart
/// Scaffold( /// Scaffold(
...@@ -235,8 +237,9 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -235,8 +237,9 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// ), /// ),
/// ], /// ],
/// ), /// ),
/// ) /// );
/// ``` /// ```
/// {@end-tool}
final List<Widget> actions; final List<Widget> actions;
/// This widget is stacked behind the toolbar and the tabbar. It's height will /// This widget is stacked behind the toolbar and the tabbar. It's height will
......
...@@ -12,9 +12,10 @@ import 'theme.dart'; ...@@ -12,9 +12,10 @@ import 'theme.dart';
/// A card is a sheet of [Material] used to represent some related information, /// A card is a sheet of [Material] used to represent some related information,
/// for example an album, a geographical location, a meal, contact details, etc. /// for example an album, a geographical location, a meal, contact details, etc.
/// ///
/// ## Sample code /// {@tool snippet --template=stateless_widget}
/// ///
/// Here is an example of using a [Card] widget. /// This sample shows creation of a [Card] widget that shows album information
/// and two actions.
/// ///
/// ```dart /// ```dart
/// Card( /// Card(
...@@ -42,10 +43,11 @@ import 'theme.dart'; ...@@ -42,10 +43,11 @@ import 'theme.dart';
/// ), /// ),
/// ], /// ],
/// ), /// ),
/// ) /// );
/// ``` /// ```
/// {@end-tool}
/// ///
/// This is what it would look like: /// This is what it looks like when run:
/// ///
/// ![A card with a slight shadow, consisting of two rows, one with an icon and /// ![A card with a slight shadow, consisting of two rows, one with an icon and
/// some text describing a musical, and the other with buttons for buying /// some text describing a musical, and the other with buttons for buying
......
...@@ -9,6 +9,9 @@ import 'list_tile.dart'; ...@@ -9,6 +9,9 @@ import 'list_tile.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
// Examples can assume:
// void setState(VoidCallback fn) { }
/// A [ListTile] with a [Checkbox]. In other words, a checkbox with a label. /// A [ListTile] with a [Checkbox]. In other words, a checkbox with a label.
/// ///
/// The entire list tile is interactive: tapping anywhere in the tile toggles /// The entire list tile is interactive: tapping anywhere in the tile toggles
......
...@@ -20,6 +20,7 @@ import 'theme.dart'; ...@@ -20,6 +20,7 @@ import 'theme.dart';
// Examples can assume: // Examples can assume:
// enum Department { treasury, state } // enum Department { treasury, state }
// BuildContext context;
/// A material design dialog. /// A material design dialog.
/// ///
......
...@@ -7,6 +7,9 @@ import 'package:flutter/painting.dart'; ...@@ -7,6 +7,9 @@ import 'package:flutter/painting.dart';
import 'theme.dart'; import 'theme.dart';
// Examples can assume:
// BuildContext context;
/// A one device pixel thick horizontal line, with padding on either /// A one device pixel thick horizontal line, with padding on either
/// side. /// side.
/// ///
......
...@@ -37,15 +37,25 @@ const double _kMinButtonSize = 48.0; ...@@ -37,15 +37,25 @@ const double _kMinButtonSize = 48.0;
/// requirements in the Material Design specification. The [alignment] controls /// requirements in the Material Design specification. The [alignment] controls
/// how the icon itself is positioned within the hit region. /// how the icon itself is positioned within the hit region.
/// ///
/// ## Sample code /// {@tool snippet --template=stateful_widget}
///
/// This sample shows an `IconButton` that uses the Material icon "volume_up" to
/// increase the volume.
///
/// ```dart preamble
/// double _volume = 0.0;
/// ```
/// ///
/// ```dart /// ```dart
/// IconButton( /// Widget build(BuildContext) {
/// return IconButton(
/// icon: Icon(Icons.volume_up), /// icon: Icon(Icons.volume_up),
/// tooltip: 'Increase volume by 10%', /// tooltip: 'Increase volume by 10%',
/// onPressed: () { setState(() { _volume *= 1.1; }); }, /// onPressed: () { setState(() { _volume *= 1.1; }); },
/// ) /// );
/// }
/// ``` /// ```
/// {@end-tool}
/// ///
/// See also: /// See also:
/// ///
......
...@@ -21,6 +21,9 @@ import 'theme.dart'; ...@@ -21,6 +21,9 @@ import 'theme.dart';
// Examples can assume: // Examples can assume:
// enum Commands { heroAndScholar, hurricaneCame } // enum Commands { heroAndScholar, hurricaneCame }
// dynamic _heroAndScholar; // dynamic _heroAndScholar;
// dynamic _selection;
// BuildContext context;
// void setState(VoidCallback fn) { }
const Duration _kMenuDuration = Duration(milliseconds: 300); const Duration _kMenuDuration = Duration(milliseconds: 300);
const double _kBaselineOffsetFromBottom = 20.0; const double _kBaselineOffsetFromBottom = 20.0;
......
...@@ -9,6 +9,9 @@ import 'radio.dart'; ...@@ -9,6 +9,9 @@ import 'radio.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
// Examples can assume:
// void setState(VoidCallback fn) { }
/// A [ListTile] with a [Radio]. In other words, a radio button with a label. /// A [ListTile] with a [Radio]. In other words, a radio button with a label.
/// ///
/// The entire list tile is interactive: tapping anywhere in the tile selects /// The entire list tile is interactive: tapping anywhere in the tile selects
......
...@@ -20,6 +20,7 @@ import 'theme.dart'; ...@@ -20,6 +20,7 @@ import 'theme.dart';
// Examples can assume: // Examples can assume:
// int _dollars = 0; // int _dollars = 0;
// int _duelCommandment = 1; // int _duelCommandment = 1;
// void setState(VoidCallback fn) { }
/// A callback that formats a numeric value from a [Slider] widget. /// A callback that formats a numeric value from a [Slider] widget.
/// ///
......
...@@ -9,6 +9,10 @@ import 'switch.dart'; ...@@ -9,6 +9,10 @@ import 'switch.dart';
import 'theme.dart'; import 'theme.dart';
import 'theme_data.dart'; import 'theme_data.dart';
// Examples can assume:
// void setState(VoidCallback fn) { }
// bool _lights;
/// A [ListTile] with a [Switch]. In other words, a switch with a label. /// A [ListTile] with a [Switch]. In other words, a switch with a label.
/// ///
/// The entire list tile is interactive: tapping anywhere in the tile toggles /// The entire list tile is interactive: tapping anywhere in the tile toggles
......
...@@ -9,6 +9,9 @@ import 'border_radius.dart'; ...@@ -9,6 +9,9 @@ import 'border_radius.dart';
import 'borders.dart'; import 'borders.dart';
import 'edge_insets.dart'; import 'edge_insets.dart';
// Examples can assume:
// BuildContext context;
/// The shape to use when rendering a [Border] or [BoxDecoration]. /// The shape to use when rendering a [Border] or [BoxDecoration].
/// ///
/// Consider using [ShapeBorder] subclasses directly (with [ShapeDecoration]), /// Consider using [ShapeBorder] subclasses directly (with [ShapeDecoration]),
......
...@@ -13,6 +13,9 @@ const String _kDefaultDebugLabel = 'unknown'; ...@@ -13,6 +13,9 @@ const String _kDefaultDebugLabel = 'unknown';
const String _kColorForegroundWarning = 'Cannot provide both a color and a foreground\n' const String _kColorForegroundWarning = 'Cannot provide both a color and a foreground\n'
'The color argument is just a shorthand for "foreground: new Paint()..color = color".'; 'The color argument is just a shorthand for "foreground: new Paint()..color = color".';
// Examples can assume:
// BuildContext context;
/// An immutable style in which paint text. /// An immutable style in which paint text.
/// ///
/// ## Sample code /// ## Sample code
......
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
import 'simulation.dart'; import 'simulation.dart';
// Examples can assume:
// AnimationController _controller;
/// A simulation that applies a constant accelerating force. /// A simulation that applies a constant accelerating force.
/// ///
/// Models a particle that follows Newton's second law of motion. The simulation /// Models a particle that follows Newton's second law of motion. The simulation
......
...@@ -10,6 +10,9 @@ import 'framework.dart'; ...@@ -10,6 +10,9 @@ import 'framework.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
import 'transitions.dart'; import 'transitions.dart';
// Examples can assume:
// bool _first;
/// Specifies which of two children to show. See [AnimatedCrossFade]. /// Specifies which of two children to show. See [AnimatedCrossFade].
/// ///
/// The child that is shown will fade in, while the other will fade out. /// The child that is shown will fade in, while the other will fade out.
......
...@@ -12,6 +12,7 @@ import 'framework.dart'; ...@@ -12,6 +12,7 @@ import 'framework.dart';
// Examples can assume: // Examples can assume:
// dynamic _lot; // dynamic _lot;
// Future<String> _calculation;
/// Base class for widgets that build themselves based on interaction with /// Base class for widgets that build themselves based on interaction with
/// a specified [Stream]. /// a specified [Stream].
......
...@@ -65,6 +65,10 @@ export 'package:flutter/rendering.dart' show ...@@ -65,6 +65,10 @@ export 'package:flutter/rendering.dart' show
// Examples can assume: // Examples can assume:
// class TestWidget extends StatelessWidget { @override Widget build(BuildContext context) => const Placeholder(); } // class TestWidget extends StatelessWidget { @override Widget build(BuildContext context) => const Placeholder(); }
// WidgetTester tester; // WidgetTester tester;
// bool _visible;
// class Sky extends CustomPainter { @override void paint(Canvas c, Size s) => null; @override bool shouldRepaint(Sky s) => false; }
// BuildContext context;
// dynamic userAvatarUrl;
// BIDIRECTIONAL TEXT SUPPORT // BIDIRECTIONAL TEXT SUPPORT
......
...@@ -10,6 +10,9 @@ import 'basic.dart'; ...@@ -10,6 +10,9 @@ import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'image.dart'; import 'image.dart';
// Examples can assume:
// BuildContext context;
/// A widget that paints a [Decoration] either before or after its child paints. /// A widget that paints a [Decoration] either before or after its child paints.
/// ///
/// [Container] insets its child by the widths of the borders; this widget does /// [Container] insets its child by the widths of the borders; this widget does
......
...@@ -34,6 +34,11 @@ export 'package:flutter/gestures.dart' show ...@@ -34,6 +34,11 @@ export 'package:flutter/gestures.dart' show
TapUpDetails, TapUpDetails,
Velocity; Velocity;
// Examples can assume:
// bool _lights;
// void setState(VoidCallback fn) { }
// String _last;
/// Factory for creating gesture recognizers. /// Factory for creating gesture recognizers.
/// ///
/// `T` is the type of gesture recognizer this class manages. /// `T` is the type of gesture recognizer this class manages.
......
...@@ -20,6 +20,7 @@ import 'ticker_provider.dart'; ...@@ -20,6 +20,7 @@ import 'ticker_provider.dart';
// class MyPage extends Placeholder { MyPage({String title}); } // class MyPage extends Placeholder { MyPage({String title}); }
// class MyHomePage extends Placeholder { } // class MyHomePage extends Placeholder { }
// NavigatorState navigator; // NavigatorState navigator;
// BuildContext context;
/// Creates a route for the given route settings. /// Creates a route for the given route settings.
/// ///
......
...@@ -8,6 +8,9 @@ import 'basic.dart'; ...@@ -8,6 +8,9 @@ import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'media_query.dart'; import 'media_query.dart';
// Examples can assume:
// String _name;
/// The text style to apply to descendant [Text] widgets without explicit style. /// The text style to apply to descendant [Text] widgets without explicit style.
class DefaultTextStyle extends InheritedWidget { class DefaultTextStyle extends InheritedWidget {
/// Creates a default text style for the given subtree. /// Creates a default text style for the given subtree.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment