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 @@
// Only code in "## Sample code" or "### Sample code" sections is examined.
// 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:
//
// * Constructor calls, typically showing what might exist in a build method.
......@@ -38,8 +43,6 @@
// top level. Instead, wrap it in a function or even a whole class, or make it a
// valid variable declaration.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
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
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');
class Line {
const Line(this.filename, this.line, this.indent);
final String filename;
final int line;
final int indent;
Line get next => this + 1;
Line operator +(int count) {
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';
void main(List<String> arguments) {
Directory flutterPackage;
if (arguments.length == 1) {
// Used for testing.
flutterPackage = Directory(arguments.single);
} else {
flutterPackage = Directory(_defaultFlutterPackage);
}
exitCode = SampleChecker(flutterPackage).checkSamples();
}
class Section {
const Section(this.start, this.preamble, this.code, this.postamble);
final Line start;
final String preamble;
final List<String> code;
final String postamble;
Iterable<String> get strings sync* {
if (preamble != null) {
assert(!preamble.contains('\n'));
yield preamble;
}
assert(!code.any((String line) => line.contains('\n')));
yield* code;
if (postamble != null) {
assert(!postamble.contains('\n'));
yield postamble;
}
}
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;
/// Checks samples and code snippets for analysis errors.
///
/// Extracts dartdoc content from flutter package source code, identifies code
/// sections, and writes them to a temporary directory, where 'flutter analyze'
/// is used to analyze the sources for problems. If problems are found, the
/// error output from the analyzer is parsed for details, and the problem
/// locations are translated back to the source location.
///
/// For snippets, the snippets are generated using the snippets tool, and they
/// 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
/// line, since snippets get reformatted when written, and the line numbers
/// don't necessarily match. It does, however, print the source of the
/// problematic line.
class SampleChecker {
SampleChecker(this._flutterPackage) {
_tempDir = Directory.systemTemp.createTempSync('flutter_analyze_sample_code.');
}
}
const String kDartDocPrefix = '///';
const String kDartDocPrefixWithSpace = '$kDartDocPrefix ';
/// The prefix of each comment line
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 {
final Directory tempDir = Directory.systemTemp.createTempSync('flutter_analyze_sample_code.');
int exitCode = 1;
bool keepMain = false;
/// A serial number so that we can create unique expression names when we
/// generate them.
int _expressionId = 0;
/// 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>[];
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 {
final File mainDart = File(path.join(tempDir.path, 'main.dart'));
final File pubSpec = File(path.join(tempDir.path, 'pubspec.yaml'));
final File analysisOptions = File(path.join(tempDir.path, 'analysis_options.yaml'));
Directory flutterPackage;
if (arguments.length == 1) {
// Used for testing.
flutterPackage = Directory(arguments.single);
final Map<String, Section> sections = <String, Section>{};
final Map<String, Snippet> snippets = <String, Snippet>{};
_extractSamples(sections, snippets);
errors = _analyze(_tempDir, sections, snippets);
} finally {
if (errors.isNotEmpty) {
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 {
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>[];
int sampleCodeSections = 0;
for (FileSystemEntity file in flutterPackage.listSync(recursive: true, followLinks: false)) {
if (file is File && path.extension(file.path) == '.dart') {
final List<String> lines = file.readAsLinesSync();
final List<Snippet> snippets = <Snippet>[];
for (File file in _listDartFiles(_flutterPackage, recursive: true)) {
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 inSampleSection = false;
bool inSnippet = false;
bool inDart = false;
bool foundDart = false;
int lineNumber = 0;
final List<String> block = <String>[];
List<String> snippetArgs = <String>[];
Line startLine;
for (String line in lines) {
for (String line in sampleLines) {
lineNumber += 1;
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) {
inPreamble = false;
processBlock(startLine, block, sections);
preambleSections.add(_processBlock(startLine, block));
block.clear();
} else if (!line.startsWith('// ')) {
throw '${file.path}:$lineNumber: Unexpected content in sample code preamble.';
throw '$relativeFilePath:$lineNumber: Unexpected content in sample code preamble.';
} else {
block.add(line.substring(3));
}
} else if (inSampleSection) {
if (!trimmedLine.startsWith(kDartDocPrefix) || trimmedLine.startsWith('/// ## ')) {
if (inDart)
throw '${file.path}:$lineNumber: Dart section inexplicably unterminated.';
if (!foundDart)
throw '${file.path}:$lineNumber: No dart block found in sample code section';
if (!trimmedLine.startsWith(_dartDocPrefix) || trimmedLine.startsWith('$_dartDocPrefix ## ')) {
if (inDart) {
throw '$relativeFilePath:$lineNumber: Dart section inexplicably unterminated.';
}
if (!foundDart) {
throw '$relativeFilePath:$lineNumber: No dart block found in sample code section';
}
inSampleSection = false;
} else {
if (inDart) {
if (trimmedLine == '/// ```') {
if (_codeBlockEndRegex.hasMatch(trimmedLine)) {
inDart = false;
processBlock(startLine, block, sections);
} else if (trimmedLine == kDartDocPrefix) {
final Section processed = _processBlock(startLine, block);
if (preambleSections.isEmpty) {
sections.add(processed);
} else {
sections.add(Section.combine(preambleSections
..toList()
..add(processed)));
}
block.clear();
} else if (trimmedLine == _dartDocPrefix) {
block.add('');
} else {
final int index = line.indexOf(kDartDocPrefixWithSpace);
if (index < 0)
throw '${file.path}:$lineNumber: Dart section inexplicably did not contain "$kDartDocPrefixWithSpace" prefix.';
final int index = line.indexOf(_dartDocPrefixWithSpace);
if (index < 0) {
throw '$relativeFilePath:$lineNumber: Dart section inexplicably did not '
'contain "$_dartDocPrefixWithSpace" prefix.';
}
block.add(line.substring(index + 4));
}
} else if (trimmedLine == '/// ```dart') {
} else if (_codeBlockStartRegex.hasMatch(trimmedLine)) {
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;
foundDart = true;
}
}
}
if (!inSampleSection) {
final Match sampleMatch = _dartDocSampleBeginRegex.firstMatch(trimmedLine);
if (line == '// Examples can assume:') {
assert(block.isEmpty);
startLine = Line(file.path, lineNumber + 1, 3);
startLine = Line('', filename: relativeFilePath, line: lineNumber + 1, indent: 3);
inPreamble = true;
} else if (trimmedLine == '/// ## Sample code' ||
trimmedLine.startsWith('/// ## Sample code:') ||
trimmedLine == '/// ### Sample code' ||
trimmedLine.startsWith('/// ### Sample code:')) {
inSampleSection = true;
trimmedLine.startsWith('/// ### Sample code:') ||
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;
sampleCodeSections += 1;
}
}
}
}
print('Found ${sections.length} sample code sections.');
for (Section section in sections) {
sectionMap[_writeSection(section).path] = section;
}
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 (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)}\';');
for (Snippet snippet in snippets) {
final File snippetFile = _writeSnippet(snippet);
snippet.contents = snippetFile.readAsLinesSync();
snippetMap[snippetFile.absolute.path] = snippet;
}
}
buffer.add('');
final List<Line> lines = List<Line>.filled(buffer.length, null, growable: true);
for (Section section in sections) {
buffer.addAll(section.strings);
lines.addAll(section.lines);
/// Helper to process arguments given as a (possibly quoted) string.
///
/// First, this will split the given [argsAsString] into separate arguments,
/// 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('''
name: analyze_sample_code
dependencies:
......@@ -220,145 +443,391 @@ linter:
- unnecessary_const
- 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,
<String>['--no-wrap', 'analyze', '--no-preamble', '--no-congratulate', mainDart.parent.path],
workingDirectory: tempDir.path,
<String>['--no-wrap', 'analyze', '--no-preamble', '--no-congratulate', '.'],
workingDirectory: directory.absolute.path,
);
final List<String> errors = <String>[];
errors.addAll(await process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).toList());
errors.add(null);
errors.addAll(await process.stdout.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).toList());
// top is stderr
if (errors.isNotEmpty && (errors.first.contains(' issues found. (ran in ') || errors.first.contains(' issue found. (ran in '))) {
errors.removeAt(0); // the "23 issues found" message goes onto stderr, which is concatenated first
if (errors.isNotEmpty && errors.last.isEmpty)
errors.removeLast(); // if there's an "issues found" message, we put a blank line on stdout before it
}
// null separates stderr from stdout
if (errors.first != null)
throw 'cannot analyze dartdocs; unexpected error output: $errors';
errors.removeAt(0);
// rest is stdout
if (errors.isNotEmpty && errors.first == 'Building flutter tool...')
errors.removeAt(0);
if (errors.isNotEmpty && errors.first.startsWith('Running "flutter packages get" in '))
errors.removeAt(0);
int errorCount = 0;
final List<String> stderr = result.stderr.toString().trim().split('\n');
final List<String> stdout = result.stdout.toString().trim().split('\n');
// Check out the stderr to see if the analyzer had it's own issues.
if (stderr.isNotEmpty && (stderr.first.contains(' issues found. (ran in ') || stderr.first.contains(' issue found. (ran in '))) {
// The "23 issues found" message goes onto stderr, which is concatenated first.
stderr.removeAt(0);
// If there's an "issues found" message, we put a blank line on stdout before it.
if (stderr.isNotEmpty && stderr.last.isEmpty) {
stderr.removeLast();
}
}
if (stderr.isNotEmpty) {
throw 'Cannot analyze dartdocs; unexpected error output:\n$stderr';
}
if (stdout.isNotEmpty && stdout.first == 'Building flutter tool...') {
stdout.removeAt(0);
}
if (stdout.isNotEmpty && stdout.first.startsWith('Running "flutter packages get" in ')) {
stdout.removeAt(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 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) {
final Match parts = errorPattern.matchAsPrefix(error);
if (parts != null) {
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 column = parts[5];
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);
if (file != 'main.dart') {
keepMain = true;
throw 'cannot analyze dartdocs; analysis errors exist in $file: $error';
if (lineNumber < 0 && errorCode == 'unused_import') {
// We don't care about unused imports.
continue;
}
if (lineNumber < 1 || lineNumber > lines.length) {
keepMain = true;
throw 'failed to parse error message (read line number as $lineNumber; total number of lines is ${lines.length}): $error';
// For when errors occur outside of the things we're trying to analyze.
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') {
// We don't really care if sample code isn't used!
} else if (actualLine == null) {
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');
errorCount += 1;
continue;
}
if (isSnippet) {
addAnalysisError(
file,
AnalysisError(
lineNumber,
columnNumber,
message,
errorCode,
null,
snippet: snippets[file.path],
),
);
} else {
print('${mainDart.path}:${lineNumber - 1}:$columnNumber: $message');
keepMain = true;
errorCount += 1;
if (lineNumber < 1 || lineNumber > fileContents.length) {
addAnalysisError(
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 {
print('${actualLine.toString(columnNumber)}: $message ($errorCode)');
errorCount += 1;
addAnalysisError(
file,
AnalysisError(
lineNumber - 1,
columnNumber,
message,
errorCode,
actualLine,
),
);
}
} else {
print('?? $error');
keepMain = true;
errorCount += 1;
addAnalysisError(
file,
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 {
try {
tempDir.deleteSync(recursive: true);
} on FileSystemException catch (e) {
print('Failed to delete ${tempDir.path}: $e');
stderr.writeln('Analyzer output: $error');
unknownAnalyzerErrors = true;
}
}
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) {
if (block.isEmpty)
/// Process one block of sample code (the part inside of "```" markers).
/// 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.';
}
if (block.first.startsWith('new ') || block.first.startsWith('const ') || block.first.startsWith(_constructorRegExp)) {
_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 ')) {
_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 ')) {
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(' = ')) {
_expressionId += 1;
sections.add(Section(line, 'void expression$_expressionId() { ', block.toList(), ' }'));
return Section.surround(line, 'void expression$_expressionId() { ', block.toList(), ' }');
} else {
final List<String> buffer = <String>[];
int subblocks = 0;
Line subline;
final List<Section> subsections = <Section>[];
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 (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;
processBlock(subline, buffer, sections);
subsections.add(_processBlock(subline, buffer));
buffer.clear();
assert(buffer.isEmpty);
subline = null;
} else if (block[index].startsWith('// ')) {
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
} else {
subline ??= line + index;
subline ??= Line(
block[index],
filename: line.filename,
line: line.line + index,
indent: line.indent,
);
buffer.add(block[index]);
}
}
if (subblocks > 0) {
if (subline != null)
processBlock(subline, buffer, sections);
if (subline != null) {
subsections.add(_processBlock(subline, buffer));
}
// Combine all of the subsections into one section, now that they've been processed.
return Section.combine(subsections);
} 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 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import 'common.dart';
void main() {
test('analyze-sample-code', () async {
final Process process = await Process.start(
test('analyze-sample-code', () {
final ProcessResult process = Process.runSync(
'../../bin/cache/dart-sdk/bin/dart',
<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> stderr = await process.stderr.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).toList();
final Match line = RegExp(r'^(.+)/main\.dart:[0-9]+:[0-9]+: .+$').matchAsPrefix(stdout[1]);
expect(line, isNot(isNull));
final String directory = line.group(1);
Directory(directory).deleteSync(recursive: true);
expect(await process.exitCode, 1);
expect(stderr, isEmpty);
expect(stdout, <String>[
'Found 2 sample code sections.',
"$directory/main.dart:1:8: Unused import: 'dart:async'",
"$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<-------',
final List<String> stdoutLines = process.stdout.toString().split('\n');
final List<String> stderrLines = process.stderr.toString().split('\n')
..removeWhere((String line) => line.startsWith('Analyzer output:'));
expect(process.exitCode, isNot(equals(0)));
expect(stderrLines, <String>[
'known_broken_documentation.dart:27:9: new Opacity(',
'>>> Unnecessary new keyword (unnecessary_new)',
'known_broken_documentation.dart:39:9: new Opacity(',
'>>> Unnecessary new keyword (unnecessary_new)',
'',
'Found 1 sample code errors.',
'',
]);
}, 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 {
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: '{{id}} Sample'),
home: new MyStatefulWidget(),
);
}
}
{{code-preamble}}
class MyHomePage extends StatelessWidget {
MyHomePage({Key key}) : super(key: key);
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key}) : super(key: key);
@override
_MyHomePageState createState() => new _MyHomePageState();
_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}
class _MyHomePageState extends State<MyHomePage> {
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
{{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 @@
import 'dart:io' hide Platform;
import 'package:args/args.dart';
import 'package:path/path.dart' as path;
import 'package:platform/platform.dart';
import 'configuration.dart';
......@@ -16,6 +17,7 @@ const String _kLibraryOption = 'library';
const String _kPackageOption = 'package';
const String _kTemplateOption = 'template';
const String _kTypeOption = 'type';
const String _kOutputOption = 'output';
/// Generates snippet dartdoc output for a given input, and creates any sample
/// applications needed by the snippet.
......@@ -44,6 +46,13 @@ void main(List<String> argList) {
defaultsTo: null,
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(
_kInputOption,
defaultsTo: environment['INPUT'],
......@@ -93,6 +102,9 @@ void main(List<String> argList) {
}
final List<String> id = <String>[];
if (args[_kOutputOption] != null) {
id.add(path.basename(path.basenameWithoutExtension(args[_kOutputOption])));
} else {
if (args[_kPackageOption] != null &&
args[_kPackageOption].isNotEmpty &&
args[_kPackageOption] != 'flutter') {
......@@ -104,12 +116,12 @@ void main(List<String> argList) {
if (args[_kElementOption] != null && args[_kElementOption].isNotEmpty) {
id.add(args[_kElementOption]);
}
if (id.isEmpty) {
errorExit('Unable to determine ID. At least one of --$_kPackageOption, '
'--$_kLibraryOption, --$_kElementOption, or the environment variables '
'PACKAGE_NAME, LIBRARY_NAME, or ELEMENT_NAME must be non-empty.');
}
}
final SnippetGenerator generator = SnippetGenerator();
stdout.write(generator.generate(
......@@ -117,6 +129,7 @@ void main(List<String> argList) {
snippetType,
template: template,
id: id.join('.'),
output: args[_kOutputOption] != null ? File(args[_kOutputOption]) : null,
));
exit(0);
}
......@@ -55,9 +55,7 @@ class SnippetGenerator {
/// "description" injection into a comment. Only used for
/// [SnippetType.application] snippets.
String interpolateTemplate(List<_ComponentTuple> injections, String template) {
final String injectionMatches =
injections.map<String>((_ComponentTuple tuple) => RegExp.escape(tuple.name)).join('|');
final RegExp moustacheRegExp = RegExp('{{($injectionMatches)}}');
final RegExp moustacheRegExp = RegExp('{{([^}]+)}}');
return template.replaceAllMapped(moustacheRegExp, (Match match) {
if (match[1] == 'description') {
// Place the description into a comment.
......@@ -77,9 +75,13 @@ class SnippetGenerator {
}
return description.join('\n').trim();
} 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
.firstWhere((_ComponentTuple tuple) => tuple.name == match[1])
.mergedContent;
.firstWhere((_ComponentTuple tuple) => tuple.name == match[1], orElse: () => null)
?.mergedContent ?? '';
}
}).trim();
}
......@@ -103,17 +105,11 @@ class SnippetGenerator {
if (result.length > 3) {
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>{
'description': injections
.firstWhere((_ComponentTuple tuple) => tuple.name == 'description')
.mergedContent,
'code': formattedCode,
'code': result.join('\n'),
}..addAll(type == SnippetType.application
? <String, String>{
'id':
......@@ -182,7 +178,7 @@ class SnippetGenerator {
/// 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
/// [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(id != null || type != SnippetType.application);
assert(input != null);
......@@ -207,11 +203,14 @@ class SnippetGenerator {
try {
app = formatter.format(app);
} on FormatterException catch (exception) {
stderr.write('Code to format:\n$app\n');
errorExit('Unable to format snippet app template: $exception');
}
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;
case SnippetType.sample:
break;
......
......@@ -8,6 +8,9 @@ import 'package:flutter/foundation.dart';
import 'tween.dart';
// Examples can assume:
// AnimationController _controller;
/// The status of an animation
enum AnimationStatus {
/// The animation is stopped at the beginning
......
......@@ -19,6 +19,7 @@ export 'package:flutter/scheduler.dart' show TickerFuture, TickerCanceled;
// Examples can assume:
// AnimationController _controller, fadeAnimationController, sizeAnimationController;
// bool dismissed;
// void setState(VoidCallback fn) { }
/// The direction in which an animation is running.
enum _AnimationDirection {
......
......@@ -11,6 +11,9 @@ import 'animation.dart';
import 'curves.dart';
import 'listener_helpers.dart';
// Examples can assume:
// AnimationController controller;
class _AlwaysCompleteAnimation extends Animation<double> {
const _AlwaysCompleteAnimation();
......
......@@ -12,6 +12,7 @@ import 'curves.dart';
// Examples can assume:
// Animation<Offset> _animation;
// AnimationController _controller;
/// An object that can produce a value of type `T` given an [Animation<double>]
/// as input.
......@@ -413,7 +414,7 @@ class ConstantTween<T> extends Tween<T> {
/// animation produced by an [AnimationController] `controller`:
///
/// ```dart
/// final Animation<double> animation = controller.drive(
/// final Animation<double> animation = _controller.drive(
/// CurveTween(curve: Curves.ease),
/// );
/// ```
......
......@@ -14,6 +14,7 @@ import 'thumb_painter.dart';
// Examples can assume:
// int _cupertinoSliderValue = 1;
// void setState(VoidCallback fn) { }
/// An iOS-style slider.
///
......
......@@ -13,6 +13,10 @@ import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'thumb_painter.dart';
// Examples can assume:
// bool _lights;
// void setState(VoidCallback fn) { }
/// An iOS-style switch.
///
/// Used to toggle the on/off state of a single setting.
......
......@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Examples can assume:
// class Cat { }
/// A category with which to annotate a class, for documentation
/// purposes.
///
......
......@@ -6,6 +6,11 @@ import 'package:meta/meta.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
/// omitted.
///
......
......@@ -219,7 +219,9 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// For less common operations, consider using a [PopupMenuButton] as the
/// last action.
///
/// ## Sample code
/// {@tool snippet --template=stateless_widget}
///
/// This sample shows adding an action to an [AppBar] that opens a shopping cart.
///
/// ```dart
/// Scaffold(
......@@ -235,8 +237,9 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// ),
/// ],
/// ),
/// )
/// );
/// ```
/// {@end-tool}
final List<Widget> actions;
/// This widget is stacked behind the toolbar and the tabbar. It's height will
......
......@@ -12,9 +12,10 @@ import 'theme.dart';
/// 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.
///
/// ## 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
/// Card(
......@@ -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
/// some text describing a musical, and the other with buttons for buying
......
......@@ -9,6 +9,9 @@ import 'list_tile.dart';
import 'theme.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.
///
/// The entire list tile is interactive: tapping anywhere in the tile toggles
......
......@@ -20,6 +20,7 @@ import 'theme.dart';
// Examples can assume:
// enum Department { treasury, state }
// BuildContext context;
/// A material design dialog.
///
......
......@@ -7,6 +7,9 @@ import 'package:flutter/painting.dart';
import 'theme.dart';
// Examples can assume:
// BuildContext context;
/// A one device pixel thick horizontal line, with padding on either
/// side.
///
......
......@@ -37,15 +37,25 @@ const double _kMinButtonSize = 48.0;
/// requirements in the Material Design specification. The [alignment] controls
/// 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
/// IconButton(
/// Widget build(BuildContext) {
/// return IconButton(
/// icon: Icon(Icons.volume_up),
/// tooltip: 'Increase volume by 10%',
/// onPressed: () { setState(() { _volume *= 1.1; }); },
/// )
/// );
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
......
......@@ -21,6 +21,9 @@ import 'theme.dart';
// Examples can assume:
// enum Commands { heroAndScholar, hurricaneCame }
// dynamic _heroAndScholar;
// dynamic _selection;
// BuildContext context;
// void setState(VoidCallback fn) { }
const Duration _kMenuDuration = Duration(milliseconds: 300);
const double _kBaselineOffsetFromBottom = 20.0;
......
......@@ -9,6 +9,9 @@ import 'radio.dart';
import 'theme.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.
///
/// The entire list tile is interactive: tapping anywhere in the tile selects
......
......@@ -20,6 +20,7 @@ import 'theme.dart';
// Examples can assume:
// int _dollars = 0;
// int _duelCommandment = 1;
// void setState(VoidCallback fn) { }
/// A callback that formats a numeric value from a [Slider] widget.
///
......
......@@ -9,6 +9,10 @@ import 'switch.dart';
import 'theme.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.
///
/// The entire list tile is interactive: tapping anywhere in the tile toggles
......
......@@ -9,6 +9,9 @@ import 'border_radius.dart';
import 'borders.dart';
import 'edge_insets.dart';
// Examples can assume:
// BuildContext context;
/// The shape to use when rendering a [Border] or [BoxDecoration].
///
/// Consider using [ShapeBorder] subclasses directly (with [ShapeDecoration]),
......
......@@ -13,6 +13,9 @@ const String _kDefaultDebugLabel = 'unknown';
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".';
// Examples can assume:
// BuildContext context;
/// An immutable style in which paint text.
///
/// ## Sample code
......
......@@ -4,6 +4,9 @@
import 'simulation.dart';
// Examples can assume:
// AnimationController _controller;
/// A simulation that applies a constant accelerating force.
///
/// Models a particle that follows Newton's second law of motion. The simulation
......
......@@ -10,6 +10,9 @@ import 'framework.dart';
import 'ticker_provider.dart';
import 'transitions.dart';
// Examples can assume:
// bool _first;
/// Specifies which of two children to show. See [AnimatedCrossFade].
///
/// The child that is shown will fade in, while the other will fade out.
......
......@@ -12,6 +12,7 @@ import 'framework.dart';
// Examples can assume:
// dynamic _lot;
// Future<String> _calculation;
/// Base class for widgets that build themselves based on interaction with
/// a specified [Stream].
......
......@@ -65,6 +65,10 @@ export 'package:flutter/rendering.dart' show
// Examples can assume:
// class TestWidget extends StatelessWidget { @override Widget build(BuildContext context) => const Placeholder(); }
// 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
......
......@@ -10,6 +10,9 @@ import 'basic.dart';
import 'framework.dart';
import 'image.dart';
// Examples can assume:
// BuildContext context;
/// 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
......
......@@ -34,6 +34,11 @@ export 'package:flutter/gestures.dart' show
TapUpDetails,
Velocity;
// Examples can assume:
// bool _lights;
// void setState(VoidCallback fn) { }
// String _last;
/// Factory for creating gesture recognizers.
///
/// `T` is the type of gesture recognizer this class manages.
......
......@@ -20,6 +20,7 @@ import 'ticker_provider.dart';
// class MyPage extends Placeholder { MyPage({String title}); }
// class MyHomePage extends Placeholder { }
// NavigatorState navigator;
// BuildContext context;
/// Creates a route for the given route settings.
///
......
......@@ -8,6 +8,9 @@ import 'basic.dart';
import 'framework.dart';
import 'media_query.dart';
// Examples can assume:
// String _name;
/// The text style to apply to descendant [Text] widgets without explicit style.
class DefaultTextStyle extends InheritedWidget {
/// 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