Unverified Commit 8bec125a authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Avoid analyzing API example code twice, clean-up (#103548)

parent 2b19ecdc
...@@ -171,10 +171,10 @@ Future<void> run(List<String> arguments) async { ...@@ -171,10 +171,10 @@ Future<void> run(List<String> arguments) async {
...arguments, ...arguments,
]); ]);
// Analyze all the sample code in the repo. // Analyze the code in `{@tool snippet}` sections in the repo.
print('$clock Sample code...'); print('$clock Snippet code...');
await runCommand(dart, await runCommand(dart,
<String>[path.join(flutterRoot, 'dev', 'bots', 'analyze_sample_code.dart'), '--verbose'], <String>[path.join(flutterRoot, 'dev', 'bots', 'analyze_snippet_code.dart'), '--verbose'],
workingDirectory: flutterRoot, workingDirectory: flutterRoot,
); );
......
...@@ -5,12 +5,9 @@ ...@@ -5,12 +5,9 @@
// See ../snippets/README.md for documentation. // See ../snippets/README.md for documentation.
// To run this, from the root of the Flutter repository: // To run this, from the root of the Flutter repository:
// bin/cache/dart-sdk/bin/dart dev/bots/analyze_sample_code.dart // bin/cache/dart-sdk/bin/dart dev/bots/analyze_snippet_code.dart
// @dart= 2.14
import 'dart:async'; import 'dart:async';
import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
...@@ -18,9 +15,6 @@ import 'package:args/args.dart'; ...@@ -18,9 +15,6 @@ import 'package:args/args.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:watcher/watcher.dart'; import 'package:watcher/watcher.dart';
// If you update this version, also update it in dev/bots/docs.sh
const String _snippetsActivateVersion = '0.2.5';
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 _defaultFlutterPackage = path.join(_flutterRoot, 'packages', 'flutter', 'lib');
final String _defaultDartUiLocation = path.join(_flutterRoot, 'bin', 'cache', 'pkg', 'sky_engine', 'lib', 'ui'); final String _defaultDartUiLocation = path.join(_flutterRoot, 'bin', 'cache', 'pkg', 'sky_engine', 'lib', 'ui');
...@@ -60,13 +54,7 @@ Future<void> main(List<String> arguments) async { ...@@ -60,13 +54,7 @@ Future<void> main(List<String> arguments) async {
argParser.addOption( argParser.addOption(
'interactive', 'interactive',
abbr: 'i', abbr: 'i',
help: 'Analyzes the sample code in the specified file interactively.', help: 'Analyzes the snippet code in the specified file interactively.',
);
argParser.addFlag(
'global-activate-snippets',
defaultsTo: true,
help: 'Whether or not to "pub global activate" the snippets package. If set, will '
'activate version $_snippetsActivateVersion',
); );
final ArgResults parsedArguments = argParser.parse(arguments); final ArgResults parsedArguments = argParser.parse(arguments);
...@@ -114,27 +102,6 @@ Future<void> main(List<String> arguments) async { ...@@ -114,27 +102,6 @@ Future<void> main(List<String> arguments) async {
tempDirectory.createSync(); tempDirectory.createSync();
} }
if (parsedArguments['global-activate-snippets']! as bool) {
try {
final ProcessResult activateResult = Process.runSync(
Platform.resolvedExecutable,
<String>[
'pub',
'global',
'activate',
'snippets',
_snippetsActivateVersion,
],
workingDirectory: _flutterRoot,
);
if (activateResult.exitCode != 0) {
exit(activateResult.exitCode);
}
} on ProcessException catch (e) {
stderr.writeln('Unable to global activate snippets package at version $_snippetsActivateVersion: $e');
exit(1);
}
}
if (parsedArguments['interactive'] != null) { if (parsedArguments['interactive'] != null) {
await _runInteractive( await _runInteractive(
tempDir: tempDirectory, tempDir: tempDirectory,
...@@ -144,110 +111,21 @@ Future<void> main(List<String> arguments) async { ...@@ -144,110 +111,21 @@ Future<void> main(List<String> arguments) async {
); );
} else { } else {
try { try {
exitCode = await SampleChecker( exitCode = await _SnippetChecker(
flutterPackage, flutterPackage,
tempDirectory: tempDirectory, tempDirectory: tempDirectory,
verbose: parsedArguments['verbose'] as bool, verbose: parsedArguments['verbose'] as bool,
dartUiLocation: includeDartUi ? dartUiLocation : null, dartUiLocation: includeDartUi ? dartUiLocation : null,
).checkSamples(); ).checkSnippets();
} on SampleCheckerException catch (e) { } on _SnippetCheckerException catch (e) {
stderr.write(e); stderr.write(e);
exit(1); exit(1);
} }
} }
} }
typedef TaskQueueClosure<T> = Future<T> Function(); class _SnippetCheckerException implements Exception {
_SnippetCheckerException(this.message, {this.file, this.line});
class _TaskQueueItem<T> {
_TaskQueueItem(this._closure, this._completer, {this.onComplete});
final TaskQueueClosure<T> _closure;
final Completer<T> _completer;
void Function()? onComplete;
Future<void> run() async {
try {
_completer.complete(await _closure());
} catch (e, st) {
_completer.completeError(e, st);
} finally {
onComplete?.call();
}
}
}
/// A task queue of Futures to be completed in parallel, throttling
/// the number of simultaneous tasks.
///
/// The tasks return results of type T.
class TaskQueue<T> {
/// Creates a task queue with a maximum number of simultaneous jobs.
/// The [maxJobs] parameter defaults to the number of CPU cores on the
/// system.
TaskQueue({int? maxJobs})
: maxJobs = maxJobs ?? Platform.numberOfProcessors;
/// The maximum number of jobs that this queue will run simultaneously.
final int maxJobs;
final Queue<_TaskQueueItem<T>> _pendingTasks = Queue<_TaskQueueItem<T>>();
final Set<_TaskQueueItem<T>> _activeTasks = <_TaskQueueItem<T>>{};
final Set<Completer<void>> _completeListeners = <Completer<void>>{};
/// Returns a future that completes when all tasks in the [TaskQueue] are
/// complete.
Future<void> get tasksComplete {
// In case this is called when there are no tasks, we want it to
// signal complete immediately.
if (_activeTasks.isEmpty && _pendingTasks.isEmpty) {
return Future<void>.value();
}
final Completer<void> completer = Completer<void>();
_completeListeners.add(completer);
return completer.future;
}
/// Adds a single closure to the task queue, returning a future that
/// completes when the task completes.
Future<T> add(TaskQueueClosure<T> task) {
final Completer<T> completer = Completer<T>();
_pendingTasks.add(_TaskQueueItem<T>(task, completer));
if (_activeTasks.length < maxJobs) {
_processTask();
}
return completer.future;
}
// Process a single task.
void _processTask() {
if (_pendingTasks.isNotEmpty && _activeTasks.length <= maxJobs) {
final _TaskQueueItem<T> item = _pendingTasks.removeFirst();
_activeTasks.add(item);
item.onComplete = () {
_activeTasks.remove(item);
_processTask();
};
item.run();
} else {
_checkForCompletion();
}
}
void _checkForCompletion() {
if (_activeTasks.isEmpty && _pendingTasks.isEmpty) {
for (final Completer<void> completer in _completeListeners) {
if (!completer.isCompleted) {
completer.complete();
}
}
_completeListeners.clear();
}
}
}
class SampleCheckerException implements Exception {
SampleCheckerException(this.message, {this.file, this.line});
final String message; final String message;
final String? file; final String? file;
final int? line; final int? line;
...@@ -264,28 +142,21 @@ class SampleCheckerException implements Exception { ...@@ -264,28 +142,21 @@ class SampleCheckerException implements Exception {
} }
} }
class AnalysisResult { class _AnalysisResult {
const AnalysisResult(this.exitCode, this.errors); const _AnalysisResult(this.exitCode, this.errors);
final int exitCode; final int exitCode;
final Map<String, List<AnalysisError>> errors; final Map<String, List<_AnalysisError>> errors;
} }
/// Checks samples and code snippets for analysis errors. /// Checks code snippets for analysis errors.
/// ///
/// Extracts dartdoc content from flutter package source code, identifies code /// Extracts dartdoc content from flutter package source code, identifies code
/// sections, and writes them to a temporary directory, where 'flutter analyze' /// sections, and writes them to a temporary directory, where 'flutter analyze'
/// is used to analyze the sources for problems. If problems are found, the /// 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 /// error output from the analyzer is parsed for details, and the problem
/// locations are translated back to the source location. /// locations are translated back to the source location.
/// class _SnippetChecker {
/// For samples, the samples are generated using the snippets tool, and they /// Creates a [_SnippetChecker].
/// are analyzed with the snippets. If errors are found in samples, then the
/// line number of the start of the sample is given instead of the actual error
/// line, since samples get reformatted when written, and the line numbers
/// don't necessarily match. It does, however, print the source of the
/// problematic line.
class SampleChecker {
/// Creates a [SampleChecker].
/// ///
/// The positional argument is the path to the package directory for the /// The positional argument is the path to the package directory for the
/// flutter package within the Flutter root dir. /// flutter package within the Flutter root dir.
...@@ -301,12 +172,12 @@ class SampleChecker { ...@@ -301,12 +172,12 @@ class SampleChecker {
/// `dart:ui` code to be analyzed along with the framework code. If not /// `dart:ui` code to be analyzed along with the framework code. If not
/// supplied, the default location of the `dart:ui` code in the Flutter /// supplied, the default location of the `dart:ui` code in the Flutter
/// repository is used (i.e. "<flutter repo>/bin/cache/pkg/sky_engine/lib/ui"). /// repository is used (i.e. "<flutter repo>/bin/cache/pkg/sky_engine/lib/ui").
SampleChecker( _SnippetChecker(
this._flutterPackage, { this._flutterPackage, {
Directory? tempDirectory, Directory? tempDirectory,
this.verbose = false, this.verbose = false,
Directory? dartUiLocation, Directory? dartUiLocation,
}) : _tempDirectory = tempDirectory ?? Directory.systemTemp.createTempSync('flutter_analyze_sample_code.'), }) : _tempDirectory = tempDirectory ?? Directory.systemTemp.createTempSync('flutter_analyze_snippet_code.'),
_keepTmp = tempDirectory != null, _keepTmp = tempDirectory != null,
_dartUiLocation = dartUiLocation; _dartUiLocation = dartUiLocation;
...@@ -316,11 +187,11 @@ class SampleChecker { ...@@ -316,11 +187,11 @@ class SampleChecker {
/// The prefix of each comment line with a space appended. /// The prefix of each comment line with a space appended.
static const String _dartDocPrefixWithSpace = '$_dartDocPrefix '; static const String _dartDocPrefixWithSpace = '$_dartDocPrefix ';
/// A RegExp that matches the beginning of a dartdoc snippet or sample. /// A RegExp that matches the beginning of a dartdoc snippet.
static final RegExp _dartDocSampleBeginRegex = RegExp(r'{@tool (sample|snippet|dartpad)(?:| ([^}]*))}'); static final RegExp _dartDocSnippetBeginRegex = RegExp(r'{@tool snippet(?:| ([^}]*))}');
/// A RegExp that matches the end of a dartdoc snippet or sample. /// A RegExp that matches the end of a dartdoc snippet.
static final RegExp _dartDocSampleEndRegex = RegExp(r'{@end-tool}'); static final RegExp _dartDocSnippetEndRegex = RegExp(r'{@end-tool}');
/// A RegExp that matches the start of a code block within dartdoc. /// A RegExp that matches the start of a code block within dartdoc.
static final RegExp _codeBlockStartRegex = RegExp(r'///\s+```dart.*$'); static final RegExp _codeBlockStartRegex = RegExp(r'///\s+```dart.*$');
...@@ -364,8 +235,8 @@ class SampleChecker { ...@@ -364,8 +235,8 @@ class SampleChecker {
return directory.listSync(recursive: recursive, followLinks: false).whereType<File>().where((File file) => path.extension(file.path) == '.dart').toList(); 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. /// Computes the headers needed for each snippet file.
List<Line> get headers { List<_Line> get headers {
return _headers ??= <String>[ return _headers ??= <String>[
'// ignore_for_file: directives_ordering', '// ignore_for_file: directives_ordering',
'// ignore_for_file: unnecessary_import', '// ignore_for_file: unnecessary_import',
...@@ -380,32 +251,31 @@ class SampleChecker { ...@@ -380,32 +251,31 @@ class SampleChecker {
"import 'package:flutter_test/flutter_test.dart';", "import 'package:flutter_test/flutter_test.dart';",
for (final File file in _listDartFiles(Directory(_defaultFlutterPackage))) for (final File file in _listDartFiles(Directory(_defaultFlutterPackage)))
"import 'package:flutter/${path.basename(file.path)}';", "import 'package:flutter/${path.basename(file.path)}';",
].map<Line>((String code) => Line.generated(code: code, filename: 'headers')).toList(); ].map<_Line>((String code) => _Line.generated(code: code, filename: 'headers')).toList();
} }
List<Line>? _headers; List<_Line>? _headers;
/// Checks all the samples in the Dart files in [_flutterPackage] for errors. /// Checks all the snippets in the Dart files in [_flutterPackage] for errors.
Future<int> checkSamples() async { Future<int> checkSnippets() async {
AnalysisResult? analysisResult; _AnalysisResult? analysisResult;
try { try {
final Map<String, Section> sections = <String, Section>{}; final Map<String, _Section> sections = <String, _Section>{};
final Map<String, Sample> snippets = <String, Sample>{};
if (_dartUiLocation != null && !_dartUiLocation!.existsSync()) { if (_dartUiLocation != null && !_dartUiLocation!.existsSync()) {
stderr.writeln('Unable to analyze engine dart samples at ${_dartUiLocation!.path}.'); stderr.writeln('Unable to analyze engine dart snippets at ${_dartUiLocation!.path}.');
} }
final List<File> filesToAnalyze = <File>[ final List<File> filesToAnalyze = <File>[
..._listDartFiles(_flutterPackage, recursive: true), ..._listDartFiles(_flutterPackage, recursive: true),
if (_dartUiLocation != null && _dartUiLocation!.existsSync()) ... _listDartFiles(_dartUiLocation!, recursive: true), if (_dartUiLocation != null && _dartUiLocation!.existsSync()) ... _listDartFiles(_dartUiLocation!, recursive: true),
]; ];
await _extractSamples(filesToAnalyze, sectionMap: sections, sampleMap: snippets); await _extractSnippets(filesToAnalyze, sectionMap: sections);
analysisResult = _analyze(_tempDirectory, sections, snippets); analysisResult = _analyze(_tempDirectory, sections);
} finally { } finally {
if (analysisResult != null && analysisResult.errors.isNotEmpty) { if (analysisResult != null && analysisResult.errors.isNotEmpty) {
for (final String filePath in analysisResult.errors.keys) { for (final String filePath in analysisResult.errors.keys) {
analysisResult.errors[filePath]!.forEach(stderr.writeln); analysisResult.errors[filePath]!.forEach(stderr.writeln);
} }
stderr.writeln('\nFound ${analysisResult.errors.length} sample code errors.'); stderr.writeln('\nFound ${analysisResult.errors.length} snippet code errors.');
} }
if (_keepTmp) { if (_keepTmp) {
print('Leaving temporary directory ${_tempDirectory.path} around for your perusal.'); print('Leaving temporary directory ${_tempDirectory.path} around for your perusal.');
...@@ -423,160 +293,39 @@ class SampleChecker { ...@@ -423,160 +293,39 @@ class SampleChecker {
/// Creates a name for the snippets tool to use for the snippet ID from a /// Creates a name for the snippets tool to use for the snippet ID from a
/// filename and starting line number. /// filename and starting line number.
String _createNameFromSource(String prefix, String filename, int start) { String _createNameFromSource(String prefix, String filename, int start) {
String sampleId = path.split(filename).join('.'); String snippetId = path.split(filename).join('.');
sampleId = path.basenameWithoutExtension(sampleId); snippetId = path.basenameWithoutExtension(snippetId);
sampleId = '$prefix.$sampleId.$start'; snippetId = '$prefix.$snippetId.$start';
return sampleId; return snippetId;
}
// The cached JSON Flutter version information from 'flutter --version --machine'.
String? _flutterVersion;
Future<Process> _runSnippetsScript(List<String> args) async {
final String workingDirectory = path.join(_flutterRoot, 'dev', 'docs');
if (_flutterVersion == null) {
// Capture the flutter version information once so that the snippets tool doesn't
// have to run it for every snippet.
if (verbose) {
print(<String>[_flutter, '--version', '--machine'].join(' '));
}
final ProcessResult versionResult = Process.runSync(_flutter, <String>['--version', '--machine']);
if (verbose) {
stdout.write(versionResult.stdout);
stderr.write(versionResult.stderr);
}
_flutterVersion = versionResult.stdout as String? ?? '';
}
if (verbose) {
print(<String>[
Platform.resolvedExecutable,
'pub',
'global',
'run',
'snippets',
...args,
].join(' '));
}
return Process.start(
Platform.resolvedExecutable,
<String>[
'pub',
'global',
'run',
'snippets',
...args,
],
workingDirectory: workingDirectory,
environment: <String, String>{
if (!Platform.environment.containsKey('FLUTTER_ROOT')) 'FLUTTER_ROOT': _flutterRoot,
if (_flutterVersion!.isNotEmpty) 'FLUTTER_VERSION': _flutterVersion!,
},
);
} }
/// Writes out the given sample to an output file in the [_tempDirectory] and /// Extracts the snippets from the Dart files in [files], writes them
/// returns the output file. /// to disk, and adds them to the [sectionMap].
Future<File> _writeSample(Sample sample) async { Future<void> _extractSnippets(
// Generate the snippet.
final String sampleId = _createNameFromSource('sample', sample.start.filename, sample.start.line);
final String inputName = '$sampleId.input';
// Now we have a filename like 'lib.src.material.foo_widget.123.dart' for each snippet.
final String inputFilePath = path.join(_tempDirectory.path, inputName);
if (verbose) {
stdout.writeln('Creating $inputFilePath.');
}
final File inputFile = File(inputFilePath)..createSync(recursive: true);
if (verbose) {
stdout.writeln('Writing $inputFilePath.');
}
inputFile.writeAsStringSync(sample.input.join('\n'));
final File outputFile = File(path.join(_tempDirectory.path, '$sampleId.dart'));
final List<String> args = <String>[
'--output=${outputFile.absolute.path}',
'--input=${inputFile.absolute.path}',
// Formatting the output will fail on analysis errors, and we want it to fail
// here, not there.
'--no-format-output',
...sample.args,
];
if (verbose) {
print('Generating sample for ${sample.start.filename}:${sample.start.line}');
}
final Process process = await _runSnippetsScript(args);
if (verbose) {
process.stdout.transform(utf8.decoder).forEach(stdout.write);
}
process.stderr.transform(utf8.decoder).forEach(stderr.write);
const Duration timeoutDuration = Duration(minutes: 5);
final int exitCode = await process.exitCode.timeout(timeoutDuration, onTimeout: () {
stderr.writeln('Snippet script timed out after $timeoutDuration.');
return -1;
});
if (exitCode != 0) {
throw SampleCheckerException(
'Unable to create sample for ${sample.start.filename}:${sample.start.line} '
'(using input from ${inputFile.path}).',
file: sample.start.filename,
line: sample.start.line,
);
}
return outputFile;
}
/// Extracts the samples from the Dart files in [files], writes them
/// to disk, and adds them to the appropriate [sectionMap] or [sampleMap].
Future<void> _extractSamples(
List<File> files, { List<File> files, {
required Map<String, Section> sectionMap, required Map<String, _Section> sectionMap,
required Map<String, Sample> sampleMap,
bool silent = false, bool silent = false,
}) async { }) async {
final List<Section> sections = <Section>[]; final List<_Section> sections = <_Section>[];
final List<Sample> samples = <Sample>[];
int dartpadCount = 0;
int sampleCount = 0;
for (final File file in files) { for (final File file in files) {
final String relativeFilePath = path.relative(file.path, from: _flutterRoot); final String relativeFilePath = path.relative(file.path, from: _flutterRoot);
final List<String> sampleLines = file.readAsLinesSync(); final List<String> snippetLine = file.readAsLinesSync();
final List<Section> preambleSections = <Section>[]; final List<_Section> preambleSections = <_Section>[];
// Whether or not we're in the file-wide preamble section ("Examples can assume"). // Whether or not we're in the file-wide preamble section ("Examples can assume").
bool inPreamble = false; bool inPreamble = false;
// Whether or not we're in a code sample // Whether or not we're in a code snippet
bool inSampleSection = false; bool inSnippetSection = false;
// Whether or not we're in a snippet code sample (with template) specifically.
bool inSnippet = false;
// Whether or not we're in a '```dart' segment. // Whether or not we're in a '```dart' segment.
bool inDart = false; bool inDart = false;
String? dartVersionOverride; String? dartVersionOverride;
int lineNumber = 0; int lineNumber = 0;
final List<String> block = <String>[]; final List<String> block = <String>[];
List<String> snippetArgs = <String>[]; late _Line startLine;
late Line startLine; for (final String line in snippetLine) {
for (final String line in sampleLines) {
lineNumber += 1; lineNumber += 1;
final String trimmedLine = line.trim(); final String trimmedLine = line.trim();
if (inSnippet) { if (inPreamble) {
if (!trimmedLine.startsWith(_dartDocPrefix)) {
throw SampleCheckerException('Snippet section unterminated.', file: relativeFilePath, line: lineNumber);
}
if (_dartDocSampleEndRegex.hasMatch(trimmedLine)) {
samples.add(
Sample(
start: startLine,
input: block,
args: snippetArgs,
serial: samples.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;
// If there's only a dartVersionOverride in the preamble, don't add // If there's only a dartVersionOverride in the preamble, don't add
...@@ -586,24 +335,24 @@ class SampleChecker { ...@@ -586,24 +335,24 @@ class SampleChecker {
} }
block.clear(); block.clear();
} else if (!line.startsWith('// ')) { } else if (!line.startsWith('// ')) {
throw SampleCheckerException('Unexpected content in sample code preamble.', file: relativeFilePath, line: lineNumber); throw _SnippetCheckerException('Unexpected content in snippet code preamble.', file: relativeFilePath, line: lineNumber);
} else if (_dartVersionRegExp.hasMatch(line)) { } else if (_dartVersionRegExp.hasMatch(line)) {
dartVersionOverride = line.substring(3); dartVersionOverride = line.substring(3);
} else { } else {
block.add(line.substring(3)); block.add(line.substring(3));
} }
} else if (inSampleSection) { } else if (inSnippetSection) {
if (_dartDocSampleEndRegex.hasMatch(trimmedLine)) { if (_dartDocSnippetEndRegex.hasMatch(trimmedLine)) {
if (inDart) { if (inDart) {
throw SampleCheckerException("Dart section didn't terminate before end of sample", file: relativeFilePath, line: lineNumber); throw _SnippetCheckerException("Dart section didn't terminate before end of snippet", file: relativeFilePath, line: lineNumber);
} }
inSampleSection = false; inSnippetSection = false;
} }
if (inDart) { if (inDart) {
if (_codeBlockEndRegex.hasMatch(trimmedLine)) { if (_codeBlockEndRegex.hasMatch(trimmedLine)) {
inDart = false; inDart = false;
final Section processed = _processBlock(startLine, block); final _Section processed = _processBlock(startLine, block);
final Section combinedSection = preambleSections.isEmpty ? processed : Section.combine(preambleSections..add(processed)); final _Section combinedSection = preambleSections.isEmpty ? processed : _Section.combine(preambleSections..add(processed));
sections.add(combinedSection.copyWith(dartVersionOverride: dartVersionOverride)); sections.add(combinedSection.copyWith(dartVersionOverride: dartVersionOverride));
block.clear(); block.clear();
} else if (trimmedLine == _dartDocPrefix) { } else if (trimmedLine == _dartDocPrefix) {
...@@ -611,7 +360,7 @@ class SampleChecker { ...@@ -611,7 +360,7 @@ class SampleChecker {
} else { } else {
final int index = line.indexOf(_dartDocPrefixWithSpace); final int index = line.indexOf(_dartDocPrefixWithSpace);
if (index < 0) { if (index < 0) {
throw SampleCheckerException( throw _SnippetCheckerException(
'Dart section inexplicably did not contain "$_dartDocPrefixWithSpace" prefix.', 'Dart section inexplicably did not contain "$_dartDocPrefixWithSpace" prefix.',
file: relativeFilePath, file: relativeFilePath,
line: lineNumber, line: lineNumber,
...@@ -621,7 +370,7 @@ class SampleChecker { ...@@ -621,7 +370,7 @@ class SampleChecker {
} }
} else if (_codeBlockStartRegex.hasMatch(trimmedLine)) { } else if (_codeBlockStartRegex.hasMatch(trimmedLine)) {
assert(block.isEmpty); assert(block.isEmpty);
startLine = Line( startLine = _Line(
filename: relativeFilePath, filename: relativeFilePath,
line: lineNumber + 1, line: lineNumber + 1,
indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length, indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length,
...@@ -629,36 +378,16 @@ class SampleChecker { ...@@ -629,36 +378,16 @@ class SampleChecker {
inDart = true; inDart = true;
} }
} }
if (!inSampleSection) { if (!inSnippetSection) {
final RegExpMatch? sampleMatch = _dartDocSampleBeginRegex.firstMatch(trimmedLine); final RegExpMatch? snippetMatch = _dartDocSnippetBeginRegex.firstMatch(trimmedLine);
if (line == '// Examples can assume:') { if (line == '// Examples can assume:') {
assert(block.isEmpty); assert(block.isEmpty);
startLine = Line.generated(filename: relativeFilePath, line: lineNumber + 1, indent: 3); startLine = _Line.generated(filename: relativeFilePath, line: lineNumber + 1, indent: 3);
inPreamble = true; inPreamble = true;
} else if (sampleMatch != null) { } else if (snippetMatch != null) {
inSnippet = sampleMatch != null && (sampleMatch[1] == 'sample' || sampleMatch[1] == 'dartpad'); inSnippetSection = true;
if (inSnippet) {
if (sampleMatch[1] == 'sample') {
sampleCount++;
}
if (sampleMatch[1] == 'dartpad') {
dartpadCount++;
}
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;
} else if (RegExp(r'///\s*#+\s+[Ss]ample\s+[Cc]ode:?$').hasMatch(trimmedLine)) { } else if (RegExp(r'///\s*#+\s+[Ss]ample\s+[Cc]ode:?$').hasMatch(trimmedLine)) {
throw SampleCheckerException( throw _SnippetCheckerException(
"Found deprecated '## Sample code' section: use {@tool snippet}...{@end-tool} instead.", "Found deprecated '## Sample code' section: use {@tool snippet}...{@end-tool} instead.",
file: relativeFilePath, file: relativeFilePath,
line: lineNumber, line: lineNumber,
...@@ -668,131 +397,59 @@ class SampleChecker { ...@@ -668,131 +397,59 @@ class SampleChecker {
} }
} }
if (!silent) if (!silent)
print('Found ${sections.length} snippet code blocks, ' print('Found ${sections.length} snippet code blocks');
'$sampleCount sample code sections, and ' for (final _Section section in sections) {
'$dartpadCount dartpad sections.');
for (final Section section in sections) {
final String path = _writeSection(section).path; final String path = _writeSection(section).path;
if (sectionMap != null) if (sectionMap != null)
sectionMap[path] = section; sectionMap[path] = section;
} }
final TaskQueue<File> sampleQueue = TaskQueue<File>();
for (final Sample sample in samples) {
final Future<File> futureFile = sampleQueue.add(() => _writeSample(sample));
if (sampleMap != null) {
sampleQueue.add(() async {
final File snippetFile = await futureFile;
sample.contents = await snippetFile.readAsLines();
sampleMap[snippetFile.absolute.path] = sample;
return futureFile;
});
}
}
await sampleQueue.tasksComplete;
}
/// 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]}';
});
} }
/// Creates the configuration files necessary for the analyzer to consider /// Creates the configuration files necessary for the analyzer to consider
/// the temporary directory a package, and sets which lint rules to enforce. /// the temporary directory a package, and sets which lint rules to enforce.
void _createConfigurationFiles(Directory directory) { void _createConfigurationFiles(Directory directory) {
final File pubSpec = File(path.join(directory.path, 'pubspec.yaml')); final File targetPubSpec = File(path.join(directory.path, 'pubspec.yaml'));
if (!pubSpec.existsSync()) { if (!targetPubSpec.existsSync()) {
pubSpec.createSync(recursive: true); // Copying pubspec.yaml from examples/api into temp directory.
final File sourcePubSpec = File(path.join(_flutterRoot, 'examples', 'api', 'pubspec.yaml'));
pubSpec.writeAsStringSync(''' if (!sourcePubSpec.existsSync()) {
name: analyze_sample_code throw 'Cannot find pubspec.yaml at ${sourcePubSpec.path}, which is also used to analyze code snippets.';
environment: }
sdk: ">=2.12.0-0 <3.0.0" sourcePubSpec.copySync(targetPubSpec.path);
dependencies: }
flutter: final File targetAnalysisOptions = File(path.join(directory.path, 'analysis_options.yaml'));
sdk: flutter if (!targetAnalysisOptions.existsSync()) {
flutter_test: // Use the same analysis_options.yaml configuration that's used for examples/api.
sdk: flutter final File sourceAnalysisOptions = File(path.join(_flutterRoot, 'examples', 'api', 'analysis_options.yaml'));
vector_math: any if (!sourceAnalysisOptions.existsSync()) {
throw 'Cannot find analysis_options.yaml at ${sourceAnalysisOptions.path}, which is also used to analyze code snippets.';
dev_dependencies: }
flutter_lints: ^2.0.0 targetAnalysisOptions
'''); ..createSync(recursive: true)
} ..writeAsStringSync('include: ${sourceAnalysisOptions.absolute.path}');
// Import the analysis options from the Flutter root.
final File analysisOptions = File(path.join(directory.path, 'analysis_options.yaml'));
if (!analysisOptions.existsSync()) {
analysisOptions.createSync(recursive: true);
analysisOptions.writeAsStringSync('''
include: package:flutter_lints/flutter.yaml
linter:
rules:
# Samples want to print things pretty often.
avoid_print: false
analyzer:
errors:
# TODO(https://github.com/flutter/flutter/issues/74381):
# Clean up existing unnecessary imports, and remove line to ignore.
unnecessary_import: ignore
''');
} }
} }
/// Writes out a sample section to the disk and returns the file. /// Writes out a snippet section to the disk and returns the file.
File _writeSection(Section section) { File _writeSection(_Section section) {
final String sectionId = _createNameFromSource('snippet', section.start.filename, section.start.line); final String sectionId = _createNameFromSource('snippet', section.start.filename, section.start.line);
final File outputFile = File(path.join(_tempDirectory.path, '$sectionId.dart'))..createSync(recursive: true); final File outputFile = File(path.join(_tempDirectory.path, '$sectionId.dart'))..createSync(recursive: true);
final List<Line> mainContents = <Line>[ final List<_Line> mainContents = <_Line>[
Line.generated(code: section.dartVersionOverride ?? '', filename: section.start.filename), _Line.generated(code: section.dartVersionOverride ?? '', filename: section.start.filename),
...headers, ...headers,
Line.generated(filename: section.start.filename), _Line.generated(filename: section.start.filename),
Line.generated(code: '// From: ${section.start.filename}:${section.start.line}', filename: section.start.filename), _Line.generated(code: '// From: ${section.start.filename}:${section.start.line}', filename: section.start.filename),
...section.code, ...section.code,
_Line.generated(filename: section.start.filename), // empty line at EOF
]; ];
outputFile.writeAsStringSync(mainContents.map<String>((Line line) => line.code).join('\n')); outputFile.writeAsStringSync(mainContents.map<String>((_Line line) => line.code).join('\n'));
return outputFile; return outputFile;
} }
/// Invokes the analyzer on the given [directory] and returns the stdout. /// Invokes the analyzer on the given [directory] and returns the stdout.
int _runAnalyzer(Directory directory, {bool silent = true, required List<String> output}) { int _runAnalyzer(Directory directory, {bool silent = true, required List<String> output}) {
if (!silent) if (!silent)
print('Starting analysis of code samples.'); print('Starting analysis of code snippets.');
_createConfigurationFiles(directory); _createConfigurationFiles(directory);
final ProcessResult result = Process.runSync( final ProcessResult result = Process.runSync(
_flutter, _flutter,
...@@ -826,23 +483,22 @@ analyzer: ...@@ -826,23 +483,22 @@ analyzer:
return result.exitCode; return result.exitCode;
} }
/// Starts the analysis phase of checking the samples by invoking the analyzer /// Starts the analysis phase of checking the snippets by invoking the analyzer
/// and parsing its output to create a map of filename to [AnalysisError]s. /// and parsing its output to create a map of filename to [_AnalysisError]s.
AnalysisResult _analyze( _AnalysisResult _analyze(
Directory directory, Directory directory,
Map<String, Section> sections, Map<String, _Section> sections, {
Map<String, Sample> samples, {
bool silent = false, bool silent = false,
}) { }) {
final List<String> errors = <String>[]; final List<String> errors = <String>[];
int exitCode = _runAnalyzer(directory, silent: silent, output: errors); int exitCode = _runAnalyzer(directory, silent: silent, output: errors);
final Map<String, List<AnalysisError>> analysisErrors = <String, List<AnalysisError>>{}; final Map<String, List<_AnalysisError>> analysisErrors = <String, List<_AnalysisError>>{};
void addAnalysisError(File file, AnalysisError error) { void addAnalysisError(File file, _AnalysisError error) {
if (analysisErrors.containsKey(file.path)) { if (analysisErrors.containsKey(file.path)) {
analysisErrors[file.path]!.add(error); analysisErrors[file.path]!.add(error);
} else { } else {
analysisErrors[file.path] = <AnalysisError>[error]; analysisErrors[file.path] = <_AnalysisError>[error];
} }
} }
...@@ -868,157 +524,117 @@ analyzer: ...@@ -868,157 +524,117 @@ analyzer:
final String message = match.namedGroup('description')!; final String message = match.namedGroup('description')!;
final File file = File(path.join(_tempDirectory.path, match.namedGroup('file'))); final File file = File(path.join(_tempDirectory.path, match.namedGroup('file')));
final List<String> fileContents = file.readAsLinesSync(); final List<String> fileContents = file.readAsLinesSync();
final bool isSnippet = path.basename(file.path).startsWith('snippet.'); final String lineString = match.namedGroup('line')!;
final bool isSample = path.basename(file.path).startsWith('sample.'); final String columnString = match.namedGroup('column')!;
final String line = match.namedGroup('line')!;
final String column = match.namedGroup('column')!;
final String errorCode = match.namedGroup('code')!; final String errorCode = match.namedGroup('code')!;
final int lineNumber = int.parse(line, radix: 10) - (isSnippet ? headerLength : 0); final int lineNumber = int.parse(lineString, radix: 10) - headerLength;
final int columnNumber = int.parse(column, radix: 10); final int columnNumber = int.parse(columnString, radix: 10);
// For when errors occur outside of the things we're trying to analyze. if (lineNumber < 1 || lineNumber > fileContents.length) {
if (!isSnippet && !isSample) {
addAnalysisError( addAnalysisError(
file, file,
AnalysisError( _AnalysisError(
type, type,
lineNumber, lineNumber,
columnNumber, columnNumber,
message, message,
errorCode, errorCode,
Line( _Line(filename: file.path, line: lineNumber),
filename: file.path,
line: lineNumber,
),
), ),
); );
throw SampleCheckerException( throw _SnippetCheckerException('Failed to parse error message: $error', file: file.path, line: lineNumber);
'Cannot analyze dartdocs; analysis errors exist: $error', }
final _Section actualSection = sections[file.path]!;
if (actualSection == null) {
throw _SnippetCheckerException(
"Unknown section for ${file.path}. Maybe the temporary directory wasn't empty?",
file: file.path, file: file.path,
line: lineNumber, line: lineNumber,
); );
} }
final _Line actualLine = actualSection.code[lineNumber - 1];
if (isSample) {
addAnalysisError( late int line;
file, late int column;
AnalysisError( String errorMessage = message;
type, _Line source = actualLine;
lineNumber, if (actualLine.generated) {
columnNumber, // Since generated lines don't appear in the original, we just provide the line
message, // in the generated file.
errorCode, line = lineNumber - 1;
null, column = columnNumber;
sample: samples[file.path], if (errorCode == 'missing_identifier' && lineNumber > 1) {
), // For a missing identifier on a generated line, it is very often because of a
); // trailing comma on the previous line, and so we want to provide a better message
} else { // and the previous line as the error location, since that appears in the original
if (lineNumber < 1 || lineNumber > fileContents.length) { // source, and can be more easily located.
addAnalysisError( final _Line previousCodeLine = sections[file.path]!.code[lineNumber - 2];
file, if (previousCodeLine.code.contains(RegExp(r',\s*$'))) {
AnalysisError( line = previousCodeLine.line;
type, column = previousCodeLine.indent + previousCodeLine.code.length - 1;
lineNumber, errorMessage = 'Unexpected comma at end of snippet code.';
columnNumber, source = previousCodeLine;
message,
errorCode,
Line(filename: file.path, line: lineNumber),
),
);
throw SampleCheckerException('Failed to parse error message: $error', file: file.path, line: lineNumber);
}
final Section actualSection = sections[file.path]!;
if (actualSection == null) {
throw SampleCheckerException(
"Unknown section for ${file.path}. Maybe the temporary directory wasn't empty?",
file: file.path,
line: lineNumber,
);
}
final Line actualLine = actualSection.code[lineNumber - 1];
late int line;
late int column;
String errorMessage = message;
Line source = actualLine;
if (actualLine.generated) {
// Since generated lines don't appear in the original, we just provide the line
// in the generated file.
line = lineNumber - 1;
column = columnNumber;
if (errorCode == 'missing_identifier' && lineNumber > 1) {
// For a missing identifier on a generated line, it is very often because of a
// trailing comma on the previous line, and so we want to provide a better message
// and the previous line as the error location, since that appears in the original
// source, and can be more easily located.
final Line previousCodeLine = sections[file.path]!.code[lineNumber - 2];
if (previousCodeLine.code.contains(RegExp(r',\s*$'))) {
line = previousCodeLine.line;
column = previousCodeLine.indent + previousCodeLine.code.length - 1;
errorMessage = 'Unexpected comma at end of sample code.';
source = previousCodeLine;
}
} }
} else {
line = actualLine.line;
column = actualLine.indent + columnNumber;
} }
addAnalysisError( } else {
file, line = actualLine.line;
AnalysisError( column = actualLine.indent + columnNumber;
type,
line,
column,
errorMessage,
errorCode,
source,
),
);
} }
addAnalysisError(
file,
_AnalysisError(
type,
line,
column,
errorMessage,
errorCode,
source,
),
);
} }
if (exitCode == 1 && analysisErrors.isEmpty && !unknownAnalyzerErrors) { if (exitCode == 1 && analysisErrors.isEmpty && !unknownAnalyzerErrors) {
exitCode = 0; exitCode = 0;
} }
if (exitCode == 0) { if (exitCode == 0) {
if (!silent) if (!silent)
print('No analysis errors in samples!'); print('No analysis errors in snippets!');
assert(analysisErrors.isEmpty); assert(analysisErrors.isEmpty);
} }
return AnalysisResult(exitCode, analysisErrors); return _AnalysisResult(exitCode, analysisErrors);
} }
/// Process one block of sample code (the part inside of "```" markers). /// Process one block of snippet code (the part inside of "```" markers).
/// Splits any sections denoted by "// ..." into separate blocks to be /// Splits any sections denoted by "// ..." into separate blocks to be
/// processed separately. Uses a primitive heuristic to make sample blocks /// processed separately. Uses a primitive heuristic to make snippet blocks
/// into valid Dart code. /// into valid Dart code.
Section _processBlock(Line line, List<String> block) { _Section _processBlock(_Line line, List<String> block) {
if (block.isEmpty) { if (block.isEmpty) {
throw SampleCheckerException('$line: Empty ```dart block in sample code.'); throw _SnippetCheckerException('$line: Empty ```dart block in snippet code.');
} }
if (block.first.startsWith('new ') || block.first.startsWith(_constructorRegExp)) { if (block.first.startsWith('new ') || block.first.startsWith(_constructorRegExp)) {
_expressionId += 1; _expressionId += 1;
return Section.surround(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;
return Section.surround(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 ')) {
return Section.fromStrings(line, block.toList()); 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;
return Section.surround(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>[]; 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 // Each section of the dart code that is either split by a blank line, or with '// ...' is
// treated as a separate code block. // treated as a separate code block.
if (block[index] == '' || block[index] == '// ...') { if (block[index] == '' || block[index] == '// ...') {
if (subline == null) if (subline == null)
throw SampleCheckerException('${Line(filename: line.filename, line: line.line + index, indent: line.indent)}: ' throw _SnippetCheckerException('${_Line(filename: line.filename, line: line.line + index, indent: line.indent)}: '
'Unexpected blank line or "// ..." line near start of subblock in sample code.'); 'Unexpected blank line or "// ..." line near start of subblock in snippet code.');
subblocks += 1; subblocks += 1;
subsections.add(_processBlock(subline, buffer)); subsections.add(_processBlock(subline, buffer));
buffer.clear(); buffer.clear();
...@@ -1028,7 +644,7 @@ analyzer: ...@@ -1028,7 +644,7 @@ analyzer:
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( subline ??= _Line(
code: block[index], code: block[index],
filename: line.filename, filename: line.filename,
line: line.line + index, line: line.line + index,
...@@ -1042,19 +658,19 @@ analyzer: ...@@ -1042,19 +658,19 @@ analyzer:
subsections.add(_processBlock(subline, buffer)); subsections.add(_processBlock(subline, buffer));
} }
// Combine all of the subsections into one section, now that they've been processed. // Combine all of the subsections into one section, now that they've been processed.
return Section.combine(subsections); return _Section.combine(subsections);
} else { } else {
return Section.fromStrings(line, block.toList()); return _Section.fromStrings(line, block.toList());
} }
} }
} }
} }
/// A class to represent a line of input code. /// A class to represent a line of input code.
class Line { class _Line {
const Line({this.code = '', required this.filename, this.line = -1, this.indent = 0}) const _Line({this.code = '', required this.filename, this.line = -1, this.indent = 0})
: generated = false; : generated = false;
const Line.generated({this.code = '', required this.filename, this.line = -1, this.indent = 0}) const _Line.generated({this.code = '', required this.filename, this.line = -1, this.indent = 0})
: generated = true; : generated = true;
/// The file that this line came from, or the file that the line was generated for, if [generated] is true. /// The file that this line came from, or the file that the line was generated for, if [generated] is true.
...@@ -1075,20 +691,20 @@ class Line { ...@@ -1075,20 +691,20 @@ class Line {
String toString() => '$filename:$line: $code'; String toString() => '$filename:$line: $code';
} }
/// A class to represent a section of sample code, marked by "{@tool snippet}...{@end-tool}". /// A class to represent a section of snippet code, marked by "{@tool snippet}...{@end-tool}".
class Section { class _Section {
const Section(this.code, {this.dartVersionOverride}); const _Section(this.code, {this.dartVersionOverride});
factory Section.combine(List<Section> sections) { factory _Section.combine(List<_Section> sections) {
final List<Line> code = sections final List<_Line> code = sections
.expand((Section section) => section.code) .expand((_Section section) => section.code)
.toList(); .toList();
return Section(code); return _Section(code);
} }
factory Section.fromStrings(Line firstLine, List<String> code) { factory _Section.fromStrings(_Line firstLine, List<String> code) {
final List<Line> codeLines = <Line>[]; final List<_Line> codeLines = <_Line>[];
for (int i = 0; i < code.length; ++i) { for (int i = 0; i < code.length; ++i) {
codeLines.add( codeLines.add(
Line( _Line(
code: code[i], code: code[i],
filename: firstLine.filename, filename: firstLine.filename,
line: firstLine.line + i, line: firstLine.line + i,
...@@ -1096,15 +712,15 @@ class Section { ...@@ -1096,15 +712,15 @@ class Section {
), ),
); );
} }
return Section(codeLines); return _Section(codeLines);
} }
factory Section.surround(Line firstLine, String prefix, List<String> code, String postfix) { factory _Section.surround(_Line firstLine, String prefix, List<String> code, String postfix) {
assert(prefix != null); assert(prefix != null);
assert(postfix != null); assert(postfix != null);
final List<Line> codeLines = <Line>[]; final List<_Line> codeLines = <_Line>[];
for (int i = 0; i < code.length; ++i) { for (int i = 0; i < code.length; ++i) {
codeLines.add( codeLines.add(
Line( _Line(
code: code[i], code: code[i],
filename: firstLine.filename, filename: firstLine.filename,
line: firstLine.line + i, line: firstLine.line + i,
...@@ -1112,82 +728,45 @@ class Section { ...@@ -1112,82 +728,45 @@ class Section {
), ),
); );
} }
return Section(<Line>[ return _Section(<_Line>[
Line.generated(code: prefix, filename: firstLine.filename, line: 0), _Line.generated(code: prefix, filename: firstLine.filename, line: 0),
...codeLines, ...codeLines,
Line.generated(code: postfix, filename: firstLine.filename, line: 0), _Line.generated(code: postfix, filename: firstLine.filename, line: 0),
]); ]);
} }
Line get start => code.firstWhere((Line line) => !line.generated); _Line get start => code.firstWhere((_Line line) => !line.generated);
final List<Line> code; final List<_Line> code;
final String? dartVersionOverride; final String? dartVersionOverride;
Section copyWith({String? dartVersionOverride}) { _Section copyWith({String? dartVersionOverride}) {
return Section(code, dartVersionOverride: dartVersionOverride ?? this.dartVersionOverride); return _Section(code, dartVersionOverride: dartVersionOverride ?? this.dartVersionOverride);
}
}
/// A class to represent a sample in the dartdoc comments, marked by
/// "{@tool sample ...}...{@end-tool}". Samples are processed separately from
/// regular snippets, because they must be injected into templates in order to be
/// analyzed.
class Sample {
Sample({
required this.start,
required List<String> input,
required List<String> args,
required this.serial,
}) : input = input.toList(),
args = args.toList(),
contents = <String>[];
final Line start;
final int serial;
List<String> input;
List<String> args;
List<String> contents;
@override
String toString() {
final StringBuffer buf = StringBuffer('sample ${args.join(' ')}\n');
int count = start.line;
for (final 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. /// 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. /// Changes how it converts to a string based on the source of the error.
class AnalysisError { class _AnalysisError {
const AnalysisError( const _AnalysisError(
this.type, this.type,
this.line, this.line,
this.column, this.column,
this.message, this.message,
this.errorCode, this.errorCode,
this.source, { this.source,
this.sample, );
});
final String type; final String type;
final int line; final int line;
final int column; final int column;
final String message; final String message;
final String errorCode; final String errorCode;
final Line? source; final _Line? source;
final Sample? sample;
@override @override
String toString() { String toString() {
if (source != null) { if (source != null) {
return '${source!.toStringWithColumn(column)}\n>>> $type: $message ($errorCode)'; return '${source!.toStringWithColumn(column)}\n>>> $type: $message ($errorCode)';
} else if (sample != null) {
return 'In sample starting at '
'${sample!.start.filename}:${sample!.start.line}:${sample!.contents[line - 1]}\n'
'>>> $type: $message ($errorCode)';
} else { } else {
return '<source unknown>:$line:$column\n>>> $type: $message ($errorCode)'; return '<source unknown>:$line:$column\n>>> $type: $message ($errorCode)';
} }
...@@ -1212,7 +791,7 @@ Future<void> _runInteractive({ ...@@ -1212,7 +791,7 @@ Future<void> _runInteractive({
} }
if (tempDir == null) { if (tempDir == null) {
tempDir = Directory.systemTemp.createTempSync('flutter_analyze_sample_code.'); tempDir = Directory.systemTemp.createTempSync('flutter_analyze_snippet_code.');
ProcessSignal.sigint.watch().listen((_) { ProcessSignal.sigint.watch().listen((_) {
print('Deleting temp files...'); print('Deleting temp files...');
tempDir!.deleteSync(recursive: true); tempDir!.deleteSync(recursive: true);
...@@ -1222,11 +801,10 @@ Future<void> _runInteractive({ ...@@ -1222,11 +801,10 @@ Future<void> _runInteractive({
} }
print('Starting up in interactive mode on ${path.relative(filePath, from: _flutterRoot)} ...'); print('Starting up in interactive mode on ${path.relative(filePath, from: _flutterRoot)} ...');
Future<void> analyze(SampleChecker checker, File file) async { Future<void> analyze(_SnippetChecker checker, File file) async {
final Map<String, Section> sections = <String, Section>{}; final Map<String, _Section> sections = <String, _Section>{};
final Map<String, Sample> snippets = <String, Sample>{}; await checker._extractSnippets(<File>[file], silent: true, sectionMap: sections);
await checker._extractSamples(<File>[file], silent: true, sectionMap: sections, sampleMap: snippets); final _AnalysisResult analysisResult = checker._analyze(checker._tempDirectory, sections, silent: true);
final AnalysisResult analysisResult = checker._analyze(checker._tempDirectory, sections, snippets, silent: true);
stderr.writeln('\u001B[2J\u001B[H'); // Clears the old results from the terminal. stderr.writeln('\u001B[2J\u001B[H'); // Clears the old results from the terminal.
if (analysisResult.errors.isNotEmpty) { if (analysisResult.errors.isNotEmpty) {
for (final String filePath in analysisResult.errors.keys) { for (final String filePath in analysisResult.errors.keys) {
...@@ -1238,7 +816,7 @@ Future<void> _runInteractive({ ...@@ -1238,7 +816,7 @@ Future<void> _runInteractive({
} }
} }
final SampleChecker checker = SampleChecker(flutterPackage, tempDirectory: tempDir) final _SnippetChecker checker = _SnippetChecker(flutterPackage, tempDirectory: tempDir)
.._createConfigurationFiles(tempDir); .._createConfigurationFiles(tempDir);
await analyze(checker, file); await analyze(checker, file);
...@@ -1248,7 +826,7 @@ Future<void> _runInteractive({ ...@@ -1248,7 +826,7 @@ Future<void> _runInteractive({
print('\n\nRerunning...'); print('\n\nRerunning...');
try { try {
analyze(checker, file); analyze(checker, file);
} on SampleCheckerException catch (e) { } on _SnippetCheckerException catch (e) {
print('Caught Exception (${e.runtimeType}), press "r" to retry:\n$e'); print('Caught Exception (${e.runtimeType}), press "r" to retry:\n$e');
} }
} }
......
...@@ -25,7 +25,6 @@ function generate_docs() { ...@@ -25,7 +25,6 @@ function generate_docs() {
# Install and activate the snippets tool, which resides in the # Install and activate the snippets tool, which resides in the
# assets-for-api-docs repo: # assets-for-api-docs repo:
# https://github.com/flutter/assets-for-api-docs/tree/master/packages/snippets # https://github.com/flutter/assets-for-api-docs/tree/master/packages/snippets
# >>> If you update this version, also update it in dev/bots/analyze_sample_code.dart <<<
"$DART" pub global activate snippets 0.2.5 "$DART" pub global activate snippets 0.2.5
# This script generates a unified doc set, and creates # This script generates a unified doc set, and creates
......
...@@ -11,7 +11,7 @@ library dart.ui; ...@@ -11,7 +11,7 @@ library dart.ui;
/// Annotation used by Flutter's Dart compiler to indicate that an /// Annotation used by Flutter's Dart compiler to indicate that an
/// [Object.toString] override should not be replaced with a supercall. /// [Object.toString] override should not be replaced with a supercall.
/// ///
/// {@tool sample --template=stateless_widget_material} /// {@tool snippet}
/// A sample if using keepToString to prevent replacement by a supercall. /// A sample if using keepToString to prevent replacement by a supercall.
/// ///
/// ```dart /// ```dart
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// 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.
// This file is used by ../analyze_sample_code_test.dart, which depends on the // This file is used by ../analyze_snippet_code_test.dart, which depends on the
// precise contents (including especially the comments) of this file. // precise contents (including especially the comments) of this file.
// Examples can assume: // Examples can assume:
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// ///
/// {@tool dartpad --template=stateless_widget_material} /// {@tool snippet}
/// Bla blabla blabla some [Text] when the `_blabla` blabla blabla is true, and /// Bla blabla blabla some [Text] when the `_blabla` blabla blabla is true, and
/// blabla it when it is blabla: /// blabla it when it is blabla:
/// ///
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// ///
/// {@tool dartpad --template=stateless_widget_material} /// {@tool snippet}
/// Dartpad with null-safe syntax /// Dartpad with null-safe syntax
/// ///
/// ```dart preamble /// ```dart preamble
......
...@@ -14,58 +14,58 @@ void main() { ...@@ -14,58 +14,58 @@ void main() {
return; return;
} }
test('analyze_sample_code smoke test', () { test('analyze_snippet_code smoke test', () {
final ProcessResult process = Process.runSync( final ProcessResult process = Process.runSync(
'../../bin/cache/dart-sdk/bin/dart', '../../bin/cache/dart-sdk/bin/dart',
<String>['analyze_sample_code.dart', '--no-include-dart-ui', 'test/analyze-sample-code-test-input'], <String>['analyze_snippet_code.dart', '--no-include-dart-ui', 'test/analyze-snippet-code-test-input'],
); );
final List<String> stdoutLines = process.stdout.toString().split('\n'); final List<String> stdoutLines = process.stdout.toString().split('\n');
final List<String> stderrLines = process.stderr.toString().split('\n'); final List<String> stderrLines = process.stderr.toString().split('\n');
expect(process.exitCode, isNot(equals(0))); expect(process.exitCode, isNot(equals(0)));
expect(stderrLines, containsAll(<Object>[ expect(stderrLines, containsAll(<Object>[
'In sample starting at dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:125: child: Text(title),', 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:138:25: child: Text(title),',
matches(RegExp(r">>> error: The final variable 'title' can't be read because (it is|it's) potentially unassigned at this point \(read_potentially_unassigned_final\)")), matches(RegExp(r">>> error: The final variable 'title' can't be read because (it is|it's) potentially unassigned at this point \(read_potentially_unassigned_final\)")),
'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:30:9: new Opacity(', 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:30:9: new Opacity(',
'>>> info: Unnecessary new keyword (unnecessary_new)', '>>> info: Unnecessary new keyword (unnecessary_new)',
'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:62:9: new Opacity(', 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:62:9: new Opacity(',
'>>> info: Unnecessary new keyword (unnecessary_new)', '>>> info: Unnecessary new keyword (unnecessary_new)',
"dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:111:9: final String? bar = 'Hello';", "dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:111:9: final String? bar = 'Hello';",
'>>> info: Prefer const over final for declarations (prefer_const_declarations)', '>>> info: Prefer const over final for declarations (prefer_const_declarations)',
'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:112:9: final int foo = null;', 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:112:9: final int foo = null;',
'>>> info: Prefer const over final for declarations (prefer_const_declarations)', '>>> info: Prefer const over final for declarations (prefer_const_declarations)',
'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:112:25: final int foo = null;', 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:112:25: final int foo = null;',
">>> error: A value of type 'Null' can't be assigned to a variable of type 'int' (invalid_assignment)", ">>> error: A value of type 'Null' can't be assigned to a variable of type 'int' (invalid_assignment)",
'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:120:24: const SizedBox(),', 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:120:24: const SizedBox(),',
'>>> error: Unexpected comma at end of sample code. (missing_identifier)', '>>> error: Unexpected comma at end of snippet code. (missing_identifier)',
'Found 2 sample code errors.', 'Found 1 snippet code errors.',
])); ]));
expect(stdoutLines, containsAll(<String>[ expect(stdoutLines, containsAll(<String>[
'Found 9 snippet code blocks, 0 sample code sections, and 2 dartpad sections.', 'Found 13 snippet code blocks',
'Starting analysis of code samples.', 'Starting analysis of code snippets.',
])); ]));
}); });
test('Analyzes dart:ui code', () { test('Analyzes dart:ui code', () {
final ProcessResult process = Process.runSync( final ProcessResult process = Process.runSync(
'../../bin/cache/dart-sdk/bin/dart', '../../bin/cache/dart-sdk/bin/dart',
<String>[ <String>[
'analyze_sample_code.dart', 'analyze_snippet_code.dart',
'--dart-ui-location=test/analyze-sample-code-test-dart-ui', '--dart-ui-location=test/analyze-snippet-code-test-dart-ui',
'test/analyze-sample-code-test-input', 'test/analyze-snippet-code-test-input',
], ],
); );
final List<String> stdoutLines = process.stdout.toString().split('\n'); final List<String> stdoutLines = process.stdout.toString().split('\n');
final List<String> stderrLines = process.stderr.toString().split('\n'); final List<String> stderrLines = process.stderr.toString().split('\n');
expect(process.exitCode, isNot(equals(0))); expect(process.exitCode, isNot(equals(0)));
expect(stderrLines, containsAll(<String>[ expect(stderrLines, containsAll(<String>[
'In sample starting at dev/bots/test/analyze-sample-code-test-dart-ui/ui.dart:15:class MyStatelessWidget extends StatelessWidget {', 'dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart:19:11: error;',
">>> error: Missing concrete implementation of 'StatelessWidget.build' (non_abstract_class_inherits_abstract_member)", ">>> error: Variables must be declared using the keywords 'const', 'final', 'var' or a type name (missing_const_final_var_or_type)",
'In sample starting at dev/bots/test/analyze-sample-code-test-dart-ui/ui.dart:15:class MyStringBuffer {', 'dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart:23:11: @keepToString',
">>> error: Classes can't be declared inside other classes (class_in_class)", ">>> error: Undefined name 'keepToString' used as an annotation (undefined_annotation)",
])); ]));
expect(stdoutLines, containsAll(<String>[ expect(stdoutLines, containsAll(<String>[
// There is one sample code section in the test's dummy dart:ui code. // There is one snippet code section in the test's dummy dart:ui code.
'Found 9 snippet code blocks, 1 sample code sections, and 2 dartpad sections.', 'Found 14 snippet code blocks',
'Starting analysis of code samples.', 'Starting analysis of code snippets.',
])); ]));
}); });
} }
# This file is also used by dev/bots/analyze_snippet_code.dart to analyze code snippets (`{@tool snippet}` sections).
include: ../../analysis_options.yaml include: ../../analysis_options.yaml
linter: linter:
rules: rules:
# Samples want to print things pretty often.
avoid_print: false
# TODO(goderbauer): enable when super params are more established and # TODO(goderbauer): enable when super params are more established and
# seeing them in the API samples is no longer surprising, # seeing them in the API samples is no longer surprising,
# https://github.com/flutter/flutter/issues/101068 # https://github.com/flutter/flutter/issues/101068
......
# This file is also used by dev/bots/analyze_snippet_code.dart to analyze code snippets (`{@tool snippet}` sections).
name: flutter_api_samples name: flutter_api_samples
description: API code samples for the Flutter repo. description: API code samples for the Flutter repo.
publish_to: 'none' publish_to: 'none'
......
...@@ -3213,7 +3213,6 @@ mixin Diagnosticable { ...@@ -3213,7 +3213,6 @@ mixin Diagnosticable {
/// value: isCurrent, /// value: isCurrent,
/// ifTrue: 'active', /// ifTrue: 'active',
/// ifFalse: 'inactive', /// ifFalse: 'inactive',
/// showName: false,
/// )); /// ));
/// ///
/// properties.add(DiagnosticsProperty<bool>('keepAlive', keepAlive)); /// properties.add(DiagnosticsProperty<bool>('keepAlive', keepAlive));
......
...@@ -171,7 +171,7 @@ class Checkbox extends StatefulWidget { ...@@ -171,7 +171,7 @@ class Checkbox extends StatefulWidget {
/// Checkbox( /// Checkbox(
/// value: true, /// value: true,
/// onChanged: (_){}, /// onChanged: (_){},
/// fillColor: MaterialStateProperty.resolveWith<Color>((states) { /// fillColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
/// if (states.contains(MaterialState.disabled)) { /// if (states.contains(MaterialState.disabled)) {
/// return Colors.orange.withOpacity(.32); /// return Colors.orange.withOpacity(.32);
/// } /// }
......
...@@ -200,7 +200,7 @@ class Radio<T> extends StatefulWidget { ...@@ -200,7 +200,7 @@ class Radio<T> extends StatefulWidget {
/// value: 1, /// value: 1,
/// groupValue: 1, /// groupValue: 1,
/// onChanged: (_){}, /// onChanged: (_){},
/// fillColor: MaterialStateProperty.resolveWith<Color>((states) { /// fillColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
/// if (states.contains(MaterialState.disabled)) { /// if (states.contains(MaterialState.disabled)) {
/// return Colors.orange.withOpacity(.32); /// return Colors.orange.withOpacity(.32);
/// } /// }
......
...@@ -238,7 +238,7 @@ class Switch extends StatelessWidget { ...@@ -238,7 +238,7 @@ class Switch extends StatelessWidget {
/// Switch( /// Switch(
/// value: true, /// value: true,
/// onChanged: (_) => true, /// onChanged: (_) => true,
/// thumbColor: MaterialStateProperty.resolveWith<Color>((states) { /// thumbColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
/// if (states.contains(MaterialState.disabled)) { /// if (states.contains(MaterialState.disabled)) {
/// return Colors.orange.withOpacity(.48); /// return Colors.orange.withOpacity(.48);
/// } /// }
...@@ -279,7 +279,7 @@ class Switch extends StatelessWidget { ...@@ -279,7 +279,7 @@ class Switch extends StatelessWidget {
/// Switch( /// Switch(
/// value: true, /// value: true,
/// onChanged: (_) => true, /// onChanged: (_) => true,
/// thumbColor: MaterialStateProperty.resolveWith<Color>((states) { /// thumbColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
/// if (states.contains(MaterialState.disabled)) { /// if (states.contains(MaterialState.disabled)) {
/// return Colors.orange.withOpacity(.48); /// return Colors.orange.withOpacity(.48);
/// } /// }
......
...@@ -201,9 +201,7 @@ enum MaterialTapTargetSize { ...@@ -201,9 +201,7 @@ enum MaterialTapTargetSize {
/// ```dart /// ```dart
/// MaterialApp( /// MaterialApp(
/// theme: ThemeData( /// theme: ThemeData(
/// colorScheme: ColorScheme.fromSwatch( /// colorScheme: ColorScheme.fromSwatch().copyWith(
/// primarySwatch: Colors.blue,
/// ).copyWith(
/// secondary: Colors.green, /// secondary: Colors.green,
/// ), /// ),
/// textTheme: const TextTheme(bodyText2: TextStyle(color: Colors.purple)), /// textTheme: const TextTheme(bodyText2: TextStyle(color: Colors.purple)),
......
...@@ -267,20 +267,20 @@ abstract class BoxBorder extends ShapeBorder { ...@@ -267,20 +267,20 @@ abstract class BoxBorder extends ShapeBorder {
/// Container( /// Container(
/// decoration: const BoxDecoration( /// decoration: const BoxDecoration(
/// border: Border( /// border: Border(
/// top: BorderSide(width: 1.0, color: Color(0xFFFFFFFF)), /// top: BorderSide(color: Color(0xFFFFFFFF)),
/// left: BorderSide(width: 1.0, color: Color(0xFFFFFFFF)), /// left: BorderSide(color: Color(0xFFFFFFFF)),
/// right: BorderSide(width: 1.0, color: Color(0xFF000000)), /// right: BorderSide(),
/// bottom: BorderSide(width: 1.0, color: Color(0xFF000000)), /// bottom: BorderSide(),
/// ), /// ),
/// ), /// ),
/// child: Container( /// child: Container(
/// padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 2.0), /// padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 2.0),
/// decoration: const BoxDecoration( /// decoration: const BoxDecoration(
/// border: Border( /// border: Border(
/// top: BorderSide(width: 1.0, color: Color(0xFFDFDFDF)), /// top: BorderSide(color: Color(0xFFDFDFDF)),
/// left: BorderSide(width: 1.0, color: Color(0xFFDFDFDF)), /// left: BorderSide(color: Color(0xFFDFDFDF)),
/// right: BorderSide(width: 1.0, color: Color(0xFF7F7F7F)), /// right: BorderSide(color: Color(0xFF7F7F7F)),
/// bottom: BorderSide(width: 1.0, color: Color(0xFF7F7F7F)), /// bottom: BorderSide(color: Color(0xFF7F7F7F)),
/// ), /// ),
/// color: Color(0xFFBFBFBF), /// color: Color(0xFFBFBFBF),
/// ), /// ),
......
...@@ -49,7 +49,6 @@ import 'image_provider.dart'; ...@@ -49,7 +49,6 @@ import 'image_provider.dart';
/// fit: BoxFit.cover, /// fit: BoxFit.cover,
/// ), /// ),
/// border: Border.all( /// border: Border.all(
/// color: Colors.black,
/// width: 8, /// width: 8,
/// ), /// ),
/// borderRadius: BorderRadius.circular(12), /// borderRadius: BorderRadius.circular(12),
......
...@@ -857,8 +857,6 @@ class RadialGradient extends Gradient { ...@@ -857,8 +857,6 @@ class RadialGradient extends Gradient {
/// decoration: const BoxDecoration( /// decoration: const BoxDecoration(
/// gradient: SweepGradient( /// gradient: SweepGradient(
/// center: FractionalOffset.center, /// center: FractionalOffset.center,
/// startAngle: 0.0,
/// endAngle: math.pi * 2,
/// colors: <Color>[ /// colors: <Color>[
/// Color(0xFF4285F4), // blue /// Color(0xFF4285F4), // blue
/// Color(0xFF34A853), // green /// Color(0xFF34A853), // green
...@@ -883,8 +881,6 @@ class RadialGradient extends Gradient { ...@@ -883,8 +881,6 @@ class RadialGradient extends Gradient {
/// decoration: const BoxDecoration( /// decoration: const BoxDecoration(
/// gradient: SweepGradient( /// gradient: SweepGradient(
/// center: FractionalOffset.center, /// center: FractionalOffset.center,
/// startAngle: 0.0,
/// endAngle: math.pi * 2,
/// colors: <Color>[ /// colors: <Color>[
/// Color(0xFF4285F4), // blue /// Color(0xFF4285F4), // blue
/// Color(0xFF34A853), // green /// Color(0xFF34A853), // green
......
...@@ -118,7 +118,6 @@ class AnnotationResult<T> { ...@@ -118,7 +118,6 @@ class AnnotationResult<T> {
/// offset, /// offset,
/// Offset.zero & size, /// Offset.zero & size,
/// super.paint, /// super.paint,
/// clipBehavior: Clip.hardEdge,
/// oldLayer: _clipRectLayer.layer, /// oldLayer: _clipRectLayer.layer,
/// ); /// );
/// } /// }
......
...@@ -2098,7 +2098,6 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ...@@ -2098,7 +2098,6 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
/// ```dart /// ```dart
/// PhysicalModel( // A /// PhysicalModel( // A
/// color: Colors.amber, /// color: Colors.amber,
/// elevation: 0.0,
/// child: Semantics( /// child: Semantics(
/// explicitChildNodes: true, /// explicitChildNodes: true,
/// child: const PhysicalModel( // B /// child: const PhysicalModel( // B
......
...@@ -198,6 +198,9 @@ class MethodChannel { ...@@ -198,6 +198,9 @@ class MethodChannel {
/// ///
/// ```dart /// ```dart
/// class Music { /// class Music {
/// // Class cannot be instantiated.
/// const Music._();
///
/// static const MethodChannel _channel = MethodChannel('music'); /// static const MethodChannel _channel = MethodChannel('music');
/// ///
/// static Future<bool> isLicensed() async { /// static Future<bool> isLicensed() async {
...@@ -213,7 +216,7 @@ class MethodChannel { ...@@ -213,7 +216,7 @@ class MethodChannel {
/// // the actual values involved would support such a typed container. /// // the actual values involved would support such a typed container.
/// // The correct type cannot be inferred with any value of `T`. /// // The correct type cannot be inferred with any value of `T`.
/// final List<dynamic>? songs = await _channel.invokeMethod<List<dynamic>>('getSongs'); /// final List<dynamic>? songs = await _channel.invokeMethod<List<dynamic>>('getSongs');
/// return songs?.map(Song.fromJson).toList() ?? <Song>[]; /// return songs?.cast<Map<String, Object?>>().map<Song>(Song.fromJson).toList() ?? <Song>[];
/// } /// }
/// ///
/// static Future<void> play(Song song, double volume) async { /// static Future<void> play(Song song, double volume) async {
...@@ -225,7 +228,7 @@ class MethodChannel { ...@@ -225,7 +228,7 @@ class MethodChannel {
/// 'volume': volume, /// 'volume': volume,
/// }); /// });
/// } on PlatformException catch (e) { /// } on PlatformException catch (e) {
/// throw 'Unable to play ${song.title}: ${e.message}'; /// throw ArgumentError('Unable to play ${song.title}: ${e.message}');
/// } /// }
/// } /// }
/// } /// }
...@@ -237,8 +240,8 @@ class MethodChannel { ...@@ -237,8 +240,8 @@ class MethodChannel {
/// final String title; /// final String title;
/// final String artist; /// final String artist;
/// ///
/// static Song fromJson(dynamic json) { /// static Song fromJson(Map<String, Object?> json) {
/// return Song(json['id'] as String, json['title'] as String, json['artist'] as String); /// return Song(json['id']! as String, json['title']! as String, json['artist']! as String);
/// } /// }
/// } /// }
/// ``` /// ```
......
...@@ -44,7 +44,6 @@ enum CrossFadeState { ...@@ -44,7 +44,6 @@ enum CrossFadeState {
/// ```dart /// ```dart
/// Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) { /// Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) {
/// return Stack( /// return Stack(
/// fit: StackFit.loose,
/// children: <Widget>[ /// children: <Widget>[
/// Positioned( /// Positioned(
/// key: bottomChildKey, /// key: bottomChildKey,
......
...@@ -4566,7 +4566,6 @@ class Flex extends MultiChildRenderObjectWidget { ...@@ -4566,7 +4566,6 @@ class Flex extends MultiChildRenderObjectWidget {
/// ), /// ),
/// Expanded( /// Expanded(
/// child: FittedBox( /// child: FittedBox(
/// fit: BoxFit.contain, // otherwise the logo will be tiny
/// child: FlutterLogo(), /// child: FlutterLogo(),
/// ), /// ),
/// ), /// ),
...@@ -4765,7 +4764,6 @@ class Row extends Flex { ...@@ -4765,7 +4764,6 @@ class Row extends Flex {
/// Text('Craft beautiful UIs'), /// Text('Craft beautiful UIs'),
/// Expanded( /// Expanded(
/// child: FittedBox( /// child: FittedBox(
/// fit: BoxFit.contain, // otherwise the logo will be tiny
/// child: FlutterLogo(), /// child: FlutterLogo(),
/// ), /// ),
/// ), /// ),
......
...@@ -11,7 +11,7 @@ import 'debug.dart'; ...@@ -11,7 +11,7 @@ import 'debug.dart';
import 'framework.dart'; import 'framework.dart';
// Examples can assume: // Examples can assume:
// class Intl { static String message(String s, { String? name, String? locale }) => ''; } // class Intl { Intl._(); static String message(String s, { String? name, String? locale }) => ''; }
// Future<void> initializeMessages(String locale) => Future<void>.value(); // Future<void> initializeMessages(String locale) => Future<void>.value();
// Used by loadAll() to record LocalizationsDelegate.load() futures we're // Used by loadAll() to record LocalizationsDelegate.load() futures we're
......
...@@ -108,7 +108,6 @@ import 'viewport.dart'; ...@@ -108,7 +108,6 @@ import 'viewport.dart';
/// ); /// );
/// } /// }
/// } /// }
///
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
class PageController extends ScrollController { class PageController extends ScrollController {
......
...@@ -27,7 +27,7 @@ import 'scroll_controller.dart'; ...@@ -27,7 +27,7 @@ import 'scroll_controller.dart';
import 'transitions.dart'; import 'transitions.dart';
// Examples can assume: // Examples can assume:
// dynamic routeObserver; // late RouteObserver<Route<void>> routeObserver;
// late NavigatorState navigator; // late NavigatorState navigator;
// late BuildContext context; // late BuildContext context;
// Future<bool> askTheUserIfTheyAreSure() async { return true; } // Future<bool> askTheUserIfTheyAreSure() async { return true; }
...@@ -1058,7 +1058,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1058,7 +1058,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// defined in the same way. /// defined in the same way.
/// ///
/// ```dart /// ```dart
/// PageRouteBuilder( /// PageRouteBuilder<void>(
/// pageBuilder: (BuildContext context, /// pageBuilder: (BuildContext context,
/// Animation<double> animation, /// Animation<double> animation,
/// Animation<double> secondaryAnimation, /// Animation<double> secondaryAnimation,
...@@ -1105,7 +1105,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -1105,7 +1105,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// route has been popped off. /// route has been popped off.
/// ///
/// ```dart /// ```dart
/// PageRouteBuilder( /// PageRouteBuilder<void>(
/// pageBuilder: (BuildContext context, /// pageBuilder: (BuildContext context,
/// Animation<double> animation, /// Animation<double> animation,
/// Animation<double> secondaryAnimation, /// Animation<double> secondaryAnimation,
......
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