From 8bec125aaf72667bf5840dca74b5916cb5dbe9ec Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 11 May 2022 21:27:45 -0700 Subject: [PATCH] Avoid analyzing API example code twice, clean-up (#103548) --- dev/bots/analyze.dart | 6 +- dev/bots/analyze_sample_code.dart | 1272 ----------------- dev/bots/analyze_snippet_code.dart | 850 +++++++++++ dev/bots/docs.sh | 1 - .../ui.dart | 2 +- .../known_broken_documentation.dart | 6 +- dev/bots/test/analyze_sample_code_test.dart | 71 - dev/bots/test/analyze_snippet_code_test.dart | 71 + examples/api/analysis_options.yaml | 4 + examples/api/pubspec.yaml | 2 + .../lib/src/foundation/diagnostics.dart | 1 - .../flutter/lib/src/material/checkbox.dart | 2 +- packages/flutter/lib/src/material/radio.dart | 2 +- packages/flutter/lib/src/material/switch.dart | 4 +- .../flutter/lib/src/material/theme_data.dart | 4 +- .../flutter/lib/src/painting/box_border.dart | 16 +- .../lib/src/painting/box_decoration.dart | 1 - .../flutter/lib/src/painting/gradient.dart | 4 - packages/flutter/lib/src/rendering/layer.dart | 1 - .../flutter/lib/src/semantics/semantics.dart | 1 - .../lib/src/services/platform_channel.dart | 11 +- .../lib/src/widgets/animated_cross_fade.dart | 1 - packages/flutter/lib/src/widgets/basic.dart | 2 - .../lib/src/widgets/localizations.dart | 2 +- .../flutter/lib/src/widgets/page_view.dart | 1 - packages/flutter/lib/src/widgets/routes.dart | 6 +- 26 files changed, 958 insertions(+), 1386 deletions(-) delete mode 100644 dev/bots/analyze_sample_code.dart create mode 100644 dev/bots/analyze_snippet_code.dart rename dev/bots/test/{analyze-sample-code-test-dart-ui => analyze-snippet-code-test-dart-ui}/ui.dart (93%) rename dev/bots/test/{analyze-sample-code-test-input => analyze-snippet-code-test-input}/known_broken_documentation.dart (94%) delete mode 100644 dev/bots/test/analyze_sample_code_test.dart create mode 100644 dev/bots/test/analyze_snippet_code_test.dart diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart index 857b2efb877..5224ac9d7dd 100644 --- a/dev/bots/analyze.dart +++ b/dev/bots/analyze.dart @@ -171,10 +171,10 @@ Future run(List arguments) async { ...arguments, ]); - // Analyze all the sample code in the repo. - print('$clock Sample code...'); + // Analyze the code in `{@tool snippet}` sections in the repo. + print('$clock Snippet code...'); await runCommand(dart, - [path.join(flutterRoot, 'dev', 'bots', 'analyze_sample_code.dart'), '--verbose'], + [path.join(flutterRoot, 'dev', 'bots', 'analyze_snippet_code.dart'), '--verbose'], workingDirectory: flutterRoot, ); diff --git a/dev/bots/analyze_sample_code.dart b/dev/bots/analyze_sample_code.dart deleted file mode 100644 index 620472b57ef..00000000000 --- a/dev/bots/analyze_sample_code.dart +++ /dev/null @@ -1,1272 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// See ../snippets/README.md for documentation. - -// To run this, from the root of the Flutter repository: -// bin/cache/dart-sdk/bin/dart dev/bots/analyze_sample_code.dart - -// @dart= 2.14 - -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; - -import 'package:args/args.dart'; -import 'package:path/path.dart' as path; -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 _defaultFlutterPackage = path.join(_flutterRoot, 'packages', 'flutter', 'lib'); -final String _defaultDartUiLocation = path.join(_flutterRoot, 'bin', 'cache', 'pkg', 'sky_engine', 'lib', 'ui'); -final String _flutter = path.join(_flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter'); - -Future main(List arguments) async { - final ArgParser argParser = ArgParser(); - argParser.addOption( - 'temp', - help: 'A location where temporary files may be written. Defaults to a ' - 'directory in the system temp folder. If specified, will not be ' - 'automatically removed at the end of execution.', - ); - argParser.addFlag( - 'verbose', - negatable: false, - help: 'Print verbose output for the analysis process.', - ); - argParser.addOption( - 'dart-ui-location', - defaultsTo: _defaultDartUiLocation, - help: 'A location where the dart:ui dart files are to be found. Defaults to ' - 'the sky_engine directory installed in this flutter repo. This ' - 'is typically the engine/src/flutter/lib/ui directory in an engine dev setup. ' - 'Implies --include-dart-ui.', - ); - argParser.addFlag( - 'include-dart-ui', - defaultsTo: true, - help: 'Includes the dart:ui code supplied by the engine in the analysis.', - ); - argParser.addFlag( - 'help', - negatable: false, - help: 'Print help for this command.', - ); - argParser.addOption( - 'interactive', - abbr: 'i', - help: 'Analyzes the sample 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); - - if (parsedArguments['help'] as bool) { - print(argParser.usage); - print('See dev/snippets/README.md for documentation.'); - exit(0); - } - - Directory flutterPackage; - if (parsedArguments.rest.length == 1) { - // Used for testing. - flutterPackage = Directory(parsedArguments.rest.single); - } else { - flutterPackage = Directory(_defaultFlutterPackage); - } - - final bool includeDartUi = parsedArguments.wasParsed('dart-ui-location') || parsedArguments['include-dart-ui'] as bool; - late Directory dartUiLocation; - if (((parsedArguments['dart-ui-location'] ?? '') as String).isNotEmpty) { - dartUiLocation = Directory( - path.absolute(parsedArguments['dart-ui-location'] as String)); - } else { - dartUiLocation = Directory(_defaultDartUiLocation); - } - if (!dartUiLocation.existsSync()) { - stderr.writeln('Unable to find dart:ui directory ${dartUiLocation.path}'); - exit(-1); - } - - Directory? tempDirectory; - if (parsedArguments.wasParsed('temp')) { - final String tempArg = parsedArguments['temp'] as String; - tempDirectory = Directory(path.join(Directory.systemTemp.absolute.path, path.basename(tempArg))); - if (path.basename(tempArg) != tempArg) { - stderr.writeln('Supplied temporary directory name should be a name, not a path. Using ${tempDirectory.absolute.path} instead.'); - } - print('Leaving temporary output in ${tempDirectory.absolute.path}.'); - // Make sure that any directory left around from a previous run is cleared - // out. - if (tempDirectory.existsSync()) { - tempDirectory.deleteSync(recursive: true); - } - tempDirectory.createSync(); - } - - if (parsedArguments['global-activate-snippets']! as bool) { - try { - final ProcessResult activateResult = Process.runSync( - Platform.resolvedExecutable, - [ - '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) { - await _runInteractive( - tempDir: tempDirectory, - flutterPackage: flutterPackage, - filePath: parsedArguments['interactive'] as String, - dartUiLocation: includeDartUi ? dartUiLocation : null, - ); - } else { - try { - exitCode = await SampleChecker( - flutterPackage, - tempDirectory: tempDirectory, - verbose: parsedArguments['verbose'] as bool, - dartUiLocation: includeDartUi ? dartUiLocation : null, - ).checkSamples(); - } on SampleCheckerException catch (e) { - stderr.write(e); - exit(1); - } - } -} - -typedef TaskQueueClosure = Future Function(); - -class _TaskQueueItem { - _TaskQueueItem(this._closure, this._completer, {this.onComplete}); - - final TaskQueueClosure _closure; - final Completer _completer; - void Function()? onComplete; - - Future 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 { - /// 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> _pendingTasks = Queue<_TaskQueueItem>(); - final Set<_TaskQueueItem> _activeTasks = <_TaskQueueItem>{}; - final Set> _completeListeners = >{}; - - /// Returns a future that completes when all tasks in the [TaskQueue] are - /// complete. - Future 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.value(); - } - final Completer completer = Completer(); - _completeListeners.add(completer); - return completer.future; - } - - /// Adds a single closure to the task queue, returning a future that - /// completes when the task completes. - Future add(TaskQueueClosure task) { - final Completer completer = Completer(); - _pendingTasks.add(_TaskQueueItem(task, completer)); - if (_activeTasks.length < maxJobs) { - _processTask(); - } - return completer.future; - } - - // Process a single task. - void _processTask() { - if (_pendingTasks.isNotEmpty && _activeTasks.length <= maxJobs) { - final _TaskQueueItem 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 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? file; - final int? line; - - @override - String toString() { - if (file != null || line != null) { - final String fileStr = file == null ? '' : '$file:'; - final String lineStr = line == null ? '' : '$line:'; - return '$fileStr$lineStr Error: $message'; - } else { - return 'Error: $message'; - } - } -} - -class AnalysisResult { - const AnalysisResult(this.exitCode, this.errors); - final int exitCode; - final Map> errors; -} - -/// 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 samples, the samples are generated using the snippets tool, and they -/// 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 - /// flutter package within the Flutter root dir. - /// - /// The optional `tempDirectory` argument supplies the location for the - /// temporary files to be written and analyzed. If not supplied, it defaults - /// to a system generated temp directory. - /// - /// The optional `verbose` argument indicates whether or not status output - /// should be emitted while doing the check. - /// - /// The optional `dartUiLocation` argument indicates the location of the - /// `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 - /// repository is used (i.e. "/bin/cache/pkg/sky_engine/lib/ui"). - SampleChecker( - this._flutterPackage, { - Directory? tempDirectory, - this.verbose = false, - Directory? dartUiLocation, - }) : _tempDirectory = tempDirectory ?? Directory.systemTemp.createTempSync('flutter_analyze_sample_code.'), - _keepTmp = tempDirectory != null, - _dartUiLocation = dartUiLocation; - - /// 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|dartpad)(?:| ([^}]*))}'); - - /// 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'///\s+```dart.*$'); - - /// A RegExp that matches the end of a code block within dartdoc. - static final RegExp _codeBlockEndRegex = RegExp(r'///\s+```\s*$'); - - /// A RegExp that matches a Dart constructor. - static final RegExp _constructorRegExp = RegExp(r'(const\s+)?_*[A-Z][a-zA-Z0-9<>._]*\('); - - /// A RegExp that matches a dart version specification in an example preamble. - static final RegExp _dartVersionRegExp = RegExp(r'\/\/ \/\/ @dart = ([0-9]+\.[0-9]+)'); - - /// Whether or not to print verbose output. - final bool verbose; - - /// Whether or not to keep the temp directory around after running. - /// - /// Defaults to false. - final bool _keepTmp; - - /// The temporary directory where all output is written. This will be deleted - /// automatically if there are no errors. - final Directory _tempDirectory; - - /// The package directory for the flutter package within the flutter root dir. - final Directory _flutterPackage; - - /// The directory for the dart:ui code to be analyzed with the flutter code. - /// - /// If this is null, then no dart:ui code is included in the analysis. It - /// defaults to the location inside of the flutter bin/cache directory that - /// contains the dart:ui code supplied by the engine. - final Directory? _dartUiLocation; - - /// A serial number so that we can create unique expression names when we - /// generate them. - int _expressionId = 0; - - static List _listDartFiles(Directory directory, {bool recursive = false}) { - return directory.listSync(recursive: recursive, followLinks: false).whereType().where((File file) => path.extension(file.path) == '.dart').toList(); - } - - /// Computes the headers needed for each sample file. - List get headers { - return _headers ??= [ - '// ignore_for_file: directives_ordering', - '// ignore_for_file: unnecessary_import', - '// ignore_for_file: unused_import', - '// ignore_for_file: unused_element', - '// ignore_for_file: unused_local_variable', - "import 'dart:async';", - "import 'dart:convert';", - "import 'dart:math' as math;", - "import 'dart:typed_data';", - "import 'dart:ui' as ui;", - "import 'package:flutter_test/flutter_test.dart';", - for (final File file in _listDartFiles(Directory(_defaultFlutterPackage))) - "import 'package:flutter/${path.basename(file.path)}';", - ].map((String code) => Line.generated(code: code, filename: 'headers')).toList(); - } - - List? _headers; - - /// Checks all the samples in the Dart files in [_flutterPackage] for errors. - Future checkSamples() async { - AnalysisResult? analysisResult; - try { - final Map sections = {}; - final Map snippets = {}; - if (_dartUiLocation != null && !_dartUiLocation!.existsSync()) { - stderr.writeln('Unable to analyze engine dart samples at ${_dartUiLocation!.path}.'); - } - final List filesToAnalyze = [ - ..._listDartFiles(_flutterPackage, recursive: true), - if (_dartUiLocation != null && _dartUiLocation!.existsSync()) ... _listDartFiles(_dartUiLocation!, recursive: true), - ]; - await _extractSamples(filesToAnalyze, sectionMap: sections, sampleMap: snippets); - analysisResult = _analyze(_tempDirectory, sections, snippets); - } finally { - if (analysisResult != null && analysisResult.errors.isNotEmpty) { - for (final String filePath in analysisResult.errors.keys) { - analysisResult.errors[filePath]!.forEach(stderr.writeln); - } - stderr.writeln('\nFound ${analysisResult.errors.length} sample code errors.'); - } - if (_keepTmp) { - print('Leaving temporary directory ${_tempDirectory.path} around for your perusal.'); - } else { - try { - _tempDirectory.deleteSync(recursive: true); - } on FileSystemException catch (e) { - stderr.writeln('Failed to delete ${_tempDirectory.path}: $e'); - } - } - } - return analysisResult.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 sampleId = path.split(filename).join('.'); - sampleId = path.basenameWithoutExtension(sampleId); - sampleId = '$prefix.$sampleId.$start'; - return sampleId; - } - - // The cached JSON Flutter version information from 'flutter --version --machine'. - String? _flutterVersion; - - Future _runSnippetsScript(List 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([_flutter, '--version', '--machine'].join(' ')); - } - final ProcessResult versionResult = Process.runSync(_flutter, ['--version', '--machine']); - if (verbose) { - stdout.write(versionResult.stdout); - stderr.write(versionResult.stderr); - } - _flutterVersion = versionResult.stdout as String? ?? ''; - } - if (verbose) { - print([ - Platform.resolvedExecutable, - 'pub', - 'global', - 'run', - 'snippets', - ...args, - ].join(' ')); - } - return Process.start( - Platform.resolvedExecutable, - [ - 'pub', - 'global', - 'run', - 'snippets', - ...args, - ], - workingDirectory: workingDirectory, - environment: { - 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 - /// returns the output file. - Future _writeSample(Sample sample) async { - // 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 args = [ - '--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 _extractSamples( - List files, { - required Map sectionMap, - required Map sampleMap, - bool silent = false, - }) async { - final List
sections =
[]; - final List samples = []; - int dartpadCount = 0; - int sampleCount = 0; - - for (final File file in files) { - final String relativeFilePath = path.relative(file.path, from: _flutterRoot); - final List sampleLines = file.readAsLinesSync(); - final List
preambleSections =
[]; - // Whether or not we're in the file-wide preamble section ("Examples can assume"). - bool inPreamble = false; - // Whether or not we're in a code sample - bool inSampleSection = 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. - bool inDart = false; - String? dartVersionOverride; - int lineNumber = 0; - final List block = []; - List snippetArgs = []; - late Line startLine; - for (final String line in sampleLines) { - lineNumber += 1; - final String trimmedLine = line.trim(); - if (inSnippet) { - 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 = []; - block.clear(); - inSnippet = false; - inSampleSection = false; - } else { - block.add(line.replaceFirst(RegExp(r'\s*/// ?'), '')); - } - } else if (inPreamble) { - if (line.isEmpty) { - inPreamble = false; - // If there's only a dartVersionOverride in the preamble, don't add - // it as a section. The dartVersionOverride was processed below. - if (dartVersionOverride == null || block.isNotEmpty) { - preambleSections.add(_processBlock(startLine, block)); - } - block.clear(); - } else if (!line.startsWith('// ')) { - throw SampleCheckerException('Unexpected content in sample code preamble.', file: relativeFilePath, line: lineNumber); - } else if (_dartVersionRegExp.hasMatch(line)) { - dartVersionOverride = line.substring(3); - } else { - block.add(line.substring(3)); - } - } else if (inSampleSection) { - if (_dartDocSampleEndRegex.hasMatch(trimmedLine)) { - if (inDart) { - throw SampleCheckerException("Dart section didn't terminate before end of sample", file: relativeFilePath, line: lineNumber); - } - inSampleSection = false; - } - if (inDart) { - if (_codeBlockEndRegex.hasMatch(trimmedLine)) { - inDart = false; - final Section processed = _processBlock(startLine, block); - final Section combinedSection = preambleSections.isEmpty ? processed : Section.combine(preambleSections..add(processed)); - sections.add(combinedSection.copyWith(dartVersionOverride: dartVersionOverride)); - block.clear(); - } else if (trimmedLine == _dartDocPrefix) { - block.add(''); - } else { - final int index = line.indexOf(_dartDocPrefixWithSpace); - if (index < 0) { - throw SampleCheckerException( - 'Dart section inexplicably did not contain "$_dartDocPrefixWithSpace" prefix.', - file: relativeFilePath, - line: lineNumber, - ); - } - block.add(line.substring(index + 4)); - } - } else if (_codeBlockStartRegex.hasMatch(trimmedLine)) { - assert(block.isEmpty); - startLine = Line( - filename: relativeFilePath, - line: lineNumber + 1, - indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length, - ); - inDart = true; - } - } - if (!inSampleSection) { - final RegExpMatch? sampleMatch = _dartDocSampleBeginRegex.firstMatch(trimmedLine); - if (line == '// Examples can assume:') { - assert(block.isEmpty); - startLine = Line.generated(filename: relativeFilePath, line: lineNumber + 1, indent: 3); - inPreamble = true; - } else if (sampleMatch != null) { - inSnippet = sampleMatch != null && (sampleMatch[1] == 'sample' || sampleMatch[1] == 'dartpad'); - 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 = []; - } - } - inSampleSection = !inSnippet; - } else if (RegExp(r'///\s*#+\s+[Ss]ample\s+[Cc]ode:?$').hasMatch(trimmedLine)) { - throw SampleCheckerException( - "Found deprecated '## Sample code' section: use {@tool snippet}...{@end-tool} instead.", - file: relativeFilePath, - line: lineNumber, - ); - } - } - } - } - if (!silent) - print('Found ${sections.length} snippet code blocks, ' - '$sampleCount sample code sections, and ' - '$dartpadCount dartpad sections.'); - for (final Section section in sections) { - final String path = _writeSection(section).path; - if (sectionMap != null) - sectionMap[path] = section; - } - final TaskQueue sampleQueue = TaskQueue(); - for (final Sample sample in samples) { - final Future 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 _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 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((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 - /// the temporary directory a package, and sets which lint rules to enforce. - void _createConfigurationFiles(Directory directory) { - final File pubSpec = File(path.join(directory.path, 'pubspec.yaml')); - if (!pubSpec.existsSync()) { - pubSpec.createSync(recursive: true); - - pubSpec.writeAsStringSync(''' -name: analyze_sample_code -environment: - sdk: ">=2.12.0-0 <3.0.0" -dependencies: - flutter: - sdk: flutter - flutter_test: - sdk: flutter - vector_math: any - -dev_dependencies: - flutter_lints: ^2.0.0 -'''); - } - - // 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. - File _writeSection(Section section) { - 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 List mainContents = [ - Line.generated(code: section.dartVersionOverride ?? '', filename: section.start.filename), - ...headers, - Line.generated(filename: section.start.filename), - Line.generated(code: '// From: ${section.start.filename}:${section.start.line}', filename: section.start.filename), - ...section.code, - ]; - outputFile.writeAsStringSync(mainContents.map((Line line) => line.code).join('\n')); - return outputFile; - } - - /// Invokes the analyzer on the given [directory] and returns the stdout. - int _runAnalyzer(Directory directory, {bool silent = true, required List output}) { - if (!silent) - print('Starting analysis of code samples.'); - _createConfigurationFiles(directory); - final ProcessResult result = Process.runSync( - _flutter, - ['--no-wrap', 'analyze', '--no-preamble', '--no-congratulate', '.'], - workingDirectory: directory.absolute.path, - ); - final List stderr = result.stderr.toString().trim().split('\n'); - final List stdout = result.stdout.toString().trim().split('\n'); - // Remove output from building the flutter tool. - stderr.removeWhere((String line) { - return line.startsWith('Building flutter tool...') - || line.startsWith('Waiting for another flutter command to release the startup lock...'); - }); - // Check out the stderr to see if the analyzer had it's own issues. - if (stderr.isNotEmpty && stderr.first.contains(RegExp(r' issues? found\. \(ran in '))) { - stderr.removeAt(0); - if (stderr.isNotEmpty && stderr.last.isEmpty) { - stderr.removeLast(); - } - } - if (stderr.isNotEmpty && stderr.any((String line) => line.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 pub get" in ')) { - stdout.removeAt(0); - } - output.addAll(stdout); - return result.exitCode; - } - - /// 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. - AnalysisResult _analyze( - Directory directory, - Map sections, - Map samples, { - bool silent = false, - }) { - final List errors = []; - int exitCode = _runAnalyzer(directory, silent: silent, output: errors); - - final Map> analysisErrors = >{}; - void addAnalysisError(File file, AnalysisError error) { - if (analysisErrors.containsKey(file.path)) { - analysisErrors[file.path]!.add(error); - } else { - analysisErrors[file.path] = [error]; - } - } - - final String kBullet = Platform.isWindows ? ' - ' : ' • '; - // 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 + 3; - for (final String error in errors) { - final RegExpMatch? match = errorPattern.firstMatch(error); - if (match == null) { - stderr.writeln('Analyzer output: $error'); - unknownAnalyzerErrors = true; - continue; - } - final String type = match.namedGroup('type')!; - final String message = match.namedGroup('description')!; - final File file = File(path.join(_tempDirectory.path, match.namedGroup('file'))); - final List fileContents = file.readAsLinesSync(); - final bool isSnippet = path.basename(file.path).startsWith('snippet.'); - final bool isSample = path.basename(file.path).startsWith('sample.'); - final String line = match.namedGroup('line')!; - final String column = match.namedGroup('column')!; - final String errorCode = match.namedGroup('code')!; - final int lineNumber = int.parse(line, radix: 10) - (isSnippet ? headerLength : 0); - final int columnNumber = int.parse(column, radix: 10); - - // For when errors occur outside of the things we're trying to analyze. - if (!isSnippet && !isSample) { - addAnalysisError( - file, - AnalysisError( - type, - lineNumber, - columnNumber, - message, - errorCode, - Line( - filename: file.path, - line: lineNumber, - ), - ), - ); - throw SampleCheckerException( - 'Cannot analyze dartdocs; analysis errors exist: $error', - file: file.path, - line: lineNumber, - ); - } - - if (isSample) { - addAnalysisError( - file, - AnalysisError( - type, - lineNumber, - columnNumber, - message, - errorCode, - null, - sample: samples[file.path], - ), - ); - } else { - if (lineNumber < 1 || lineNumber > fileContents.length) { - addAnalysisError( - file, - AnalysisError( - type, - lineNumber, - columnNumber, - 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( - file, - AnalysisError( - type, - line, - column, - errorMessage, - errorCode, - source, - ), - ); - } - } - if (exitCode == 1 && analysisErrors.isEmpty && !unknownAnalyzerErrors) { - exitCode = 0; - } - if (exitCode == 0) { - if (!silent) - print('No analysis errors in samples!'); - assert(analysisErrors.isEmpty); - } - return AnalysisResult(exitCode, analysisErrors); - } - - /// 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 block) { - if (block.isEmpty) { - throw SampleCheckerException('$line: Empty ```dart block in sample code.'); - } - if (block.first.startsWith('new ') || block.first.startsWith(_constructorRegExp)) { - _expressionId += 1; - return Section.surround(line, 'dynamic expression$_expressionId = ', block.toList(), ';'); - } else if (block.first.startsWith('await ')) { - _expressionId += 1; - return Section.surround(line, 'Future expression$_expressionId() async { ', block.toList(), ' }'); - } else if (block.first.startsWith('class ') || block.first.startsWith('enum ')) { - return Section.fromStrings(line, block.toList()); - } else if ((block.first.startsWith('_') || block.first.startsWith('final ')) && block.first.contains(' = ')) { - _expressionId += 1; - return Section.surround(line, 'void expression$_expressionId() { ', block.toList(), ' }'); - } else { - final List buffer = []; - int subblocks = 0; - Line? subline; - final List
subsections =
[]; - 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 SampleCheckerException('${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; - 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( - code: block[index], - filename: line.filename, - line: line.line + index, - indent: line.indent, - ); - buffer.add(block[index]); - } - } - if (subblocks > 0) { - 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 { - return Section.fromStrings(line, block.toList()); - } - } - } -} - -/// A class to represent a line of input code. -class Line { - const Line({this.code = '', required this.filename, this.line = -1, this.indent = 0}) - : generated = false; - const Line.generated({this.code = '', required this.filename, this.line = -1, this.indent = 0}) - : generated = true; - - /// The file that this line came from, or the file that the line was generated for, if [generated] is true. - final String filename; - final int line; - final int indent; - final String code; - final bool generated; - - String toStringWithColumn(int column) { - if (column != null && indent != null) { - return '$filename:$line:${column + indent}: $code'; - } - return toString(); - } - - @override - String toString() => '$filename:$line: $code'; -} - -/// A class to represent a section of sample code, marked by "{@tool snippet}...{@end-tool}". -class Section { - const Section(this.code, {this.dartVersionOverride}); - factory Section.combine(List
sections) { - final List code = sections - .expand((Section section) => section.code) - .toList(); - return Section(code); - } - factory Section.fromStrings(Line firstLine, List code) { - final List codeLines = []; - for (int i = 0; i < code.length; ++i) { - codeLines.add( - Line( - code: code[i], - filename: firstLine.filename, - line: firstLine.line + i, - indent: firstLine.indent, - ), - ); - } - return Section(codeLines); - } - factory Section.surround(Line firstLine, String prefix, List code, String postfix) { - assert(prefix != null); - assert(postfix != null); - final List codeLines = []; - for (int i = 0; i < code.length; ++i) { - codeLines.add( - Line( - code: code[i], - filename: firstLine.filename, - line: firstLine.line + i, - indent: firstLine.indent, - ), - ); - } - return Section([ - Line.generated(code: prefix, filename: firstLine.filename, line: 0), - ...codeLines, - Line.generated(code: postfix, filename: firstLine.filename, line: 0), - ]); - } - Line get start => code.firstWhere((Line line) => !line.generated); - final List code; - final String? dartVersionOverride; - - Section copyWith({String? 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 input, - required List args, - required this.serial, - }) : input = input.toList(), - args = args.toList(), - contents = []; - final Line start; - final int serial; - List input; - List args; - List 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. -/// -/// Changes how it converts to a string based on the source of the error. -class AnalysisError { - const AnalysisError( - this.type, - this.line, - this.column, - this.message, - this.errorCode, - this.source, { - this.sample, - }); - - final String type; - final int line; - final int column; - final String message; - final String errorCode; - final Line? source; - final Sample? sample; - - @override - String toString() { - if (source != null) { - 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 { - return ':$line:$column\n>>> $type: $message ($errorCode)'; - } - } -} - -Future _runInteractive({ - required Directory? tempDir, - required Directory flutterPackage, - required String filePath, - required Directory? dartUiLocation, -}) async { - filePath = path.isAbsolute(filePath) ? filePath : path.join(path.current, filePath); - final File file = File(filePath); - if (!file.existsSync()) { - throw 'Path ${file.absolute.path} does not exist ($filePath).'; - } - if (!path.isWithin(_flutterRoot, file.absolute.path) && - (dartUiLocation == null || !path.isWithin(dartUiLocation.path, file.absolute.path))) { - throw 'Path ${file.absolute.path} is not within the flutter root: ' - '$_flutterRoot${dartUiLocation != null ? ' or the dart:ui location: $dartUiLocation' : ''}'; - } - - if (tempDir == null) { - tempDir = Directory.systemTemp.createTempSync('flutter_analyze_sample_code.'); - ProcessSignal.sigint.watch().listen((_) { - print('Deleting temp files...'); - tempDir!.deleteSync(recursive: true); - exit(0); - }); - print('Using temp dir ${tempDir.path}'); - } - print('Starting up in interactive mode on ${path.relative(filePath, from: _flutterRoot)} ...'); - - Future analyze(SampleChecker checker, File file) async { - final Map sections = {}; - final Map snippets = {}; - await checker._extractSamples([file], silent: true, sectionMap: sections, sampleMap: snippets); - final AnalysisResult analysisResult = checker._analyze(checker._tempDirectory, sections, snippets, silent: true); - stderr.writeln('\u001B[2J\u001B[H'); // Clears the old results from the terminal. - if (analysisResult.errors.isNotEmpty) { - for (final String filePath in analysisResult.errors.keys) { - analysisResult.errors[filePath]!.forEach(stderr.writeln); - } - stderr.writeln('\nFound ${analysisResult.errors.length} errors.'); - } else { - stderr.writeln('\nNo issues found.'); - } - } - - final SampleChecker checker = SampleChecker(flutterPackage, tempDirectory: tempDir) - .._createConfigurationFiles(tempDir); - await analyze(checker, file); - - print('Type "q" to quit, or "r" to delete temp dir and manually reload.'); - - void rerun() { - print('\n\nRerunning...'); - try { - analyze(checker, file); - } on SampleCheckerException catch (e) { - print('Caught Exception (${e.runtimeType}), press "r" to retry:\n$e'); - } - } - - stdin.lineMode = false; - stdin.echoMode = false; - stdin.transform(utf8.decoder).listen((String input) { - switch (input) { - case 'q': - print('Exiting...'); - exit(0); - case 'r': - print('Deleting temp files...'); - tempDir!.deleteSync(recursive: true); - rerun(); - break; - } - }); - - Watcher(file.absolute.path).events.listen((_) => rerun()); -} diff --git a/dev/bots/analyze_snippet_code.dart b/dev/bots/analyze_snippet_code.dart new file mode 100644 index 00000000000..7d09cc947e9 --- /dev/null +++ b/dev/bots/analyze_snippet_code.dart @@ -0,0 +1,850 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// See ../snippets/README.md for documentation. + +// To run this, from the root of the Flutter repository: +// bin/cache/dart-sdk/bin/dart dev/bots/analyze_snippet_code.dart + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:path/path.dart' as path; +import 'package:watcher/watcher.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 _defaultDartUiLocation = path.join(_flutterRoot, 'bin', 'cache', 'pkg', 'sky_engine', 'lib', 'ui'); +final String _flutter = path.join(_flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter'); + +Future main(List arguments) async { + final ArgParser argParser = ArgParser(); + argParser.addOption( + 'temp', + help: 'A location where temporary files may be written. Defaults to a ' + 'directory in the system temp folder. If specified, will not be ' + 'automatically removed at the end of execution.', + ); + argParser.addFlag( + 'verbose', + negatable: false, + help: 'Print verbose output for the analysis process.', + ); + argParser.addOption( + 'dart-ui-location', + defaultsTo: _defaultDartUiLocation, + help: 'A location where the dart:ui dart files are to be found. Defaults to ' + 'the sky_engine directory installed in this flutter repo. This ' + 'is typically the engine/src/flutter/lib/ui directory in an engine dev setup. ' + 'Implies --include-dart-ui.', + ); + argParser.addFlag( + 'include-dart-ui', + defaultsTo: true, + help: 'Includes the dart:ui code supplied by the engine in the analysis.', + ); + argParser.addFlag( + 'help', + negatable: false, + help: 'Print help for this command.', + ); + argParser.addOption( + 'interactive', + abbr: 'i', + help: 'Analyzes the snippet code in the specified file interactively.', + ); + + final ArgResults parsedArguments = argParser.parse(arguments); + + if (parsedArguments['help'] as bool) { + print(argParser.usage); + print('See dev/snippets/README.md for documentation.'); + exit(0); + } + + Directory flutterPackage; + if (parsedArguments.rest.length == 1) { + // Used for testing. + flutterPackage = Directory(parsedArguments.rest.single); + } else { + flutterPackage = Directory(_defaultFlutterPackage); + } + + final bool includeDartUi = parsedArguments.wasParsed('dart-ui-location') || parsedArguments['include-dart-ui'] as bool; + late Directory dartUiLocation; + if (((parsedArguments['dart-ui-location'] ?? '') as String).isNotEmpty) { + dartUiLocation = Directory( + path.absolute(parsedArguments['dart-ui-location'] as String)); + } else { + dartUiLocation = Directory(_defaultDartUiLocation); + } + if (!dartUiLocation.existsSync()) { + stderr.writeln('Unable to find dart:ui directory ${dartUiLocation.path}'); + exit(-1); + } + + Directory? tempDirectory; + if (parsedArguments.wasParsed('temp')) { + final String tempArg = parsedArguments['temp'] as String; + tempDirectory = Directory(path.join(Directory.systemTemp.absolute.path, path.basename(tempArg))); + if (path.basename(tempArg) != tempArg) { + stderr.writeln('Supplied temporary directory name should be a name, not a path. Using ${tempDirectory.absolute.path} instead.'); + } + print('Leaving temporary output in ${tempDirectory.absolute.path}.'); + // Make sure that any directory left around from a previous run is cleared + // out. + if (tempDirectory.existsSync()) { + tempDirectory.deleteSync(recursive: true); + } + tempDirectory.createSync(); + } + + if (parsedArguments['interactive'] != null) { + await _runInteractive( + tempDir: tempDirectory, + flutterPackage: flutterPackage, + filePath: parsedArguments['interactive'] as String, + dartUiLocation: includeDartUi ? dartUiLocation : null, + ); + } else { + try { + exitCode = await _SnippetChecker( + flutterPackage, + tempDirectory: tempDirectory, + verbose: parsedArguments['verbose'] as bool, + dartUiLocation: includeDartUi ? dartUiLocation : null, + ).checkSnippets(); + } on _SnippetCheckerException catch (e) { + stderr.write(e); + exit(1); + } + } +} + +class _SnippetCheckerException implements Exception { + _SnippetCheckerException(this.message, {this.file, this.line}); + final String message; + final String? file; + final int? line; + + @override + String toString() { + if (file != null || line != null) { + final String fileStr = file == null ? '' : '$file:'; + final String lineStr = line == null ? '' : '$line:'; + return '$fileStr$lineStr Error: $message'; + } else { + return 'Error: $message'; + } + } +} + +class _AnalysisResult { + const _AnalysisResult(this.exitCode, this.errors); + final int exitCode; + final Map> errors; +} + +/// Checks 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. +class _SnippetChecker { + /// Creates a [_SnippetChecker]. + /// + /// The positional argument is the path to the package directory for the + /// flutter package within the Flutter root dir. + /// + /// The optional `tempDirectory` argument supplies the location for the + /// temporary files to be written and analyzed. If not supplied, it defaults + /// to a system generated temp directory. + /// + /// The optional `verbose` argument indicates whether or not status output + /// should be emitted while doing the check. + /// + /// The optional `dartUiLocation` argument indicates the location of the + /// `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 + /// repository is used (i.e. "/bin/cache/pkg/sky_engine/lib/ui"). + _SnippetChecker( + this._flutterPackage, { + Directory? tempDirectory, + this.verbose = false, + Directory? dartUiLocation, + }) : _tempDirectory = tempDirectory ?? Directory.systemTemp.createTempSync('flutter_analyze_snippet_code.'), + _keepTmp = tempDirectory != null, + _dartUiLocation = dartUiLocation; + + /// 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. + static final RegExp _dartDocSnippetBeginRegex = RegExp(r'{@tool snippet(?:| ([^}]*))}'); + + /// A RegExp that matches the end of a dartdoc snippet. + static final RegExp _dartDocSnippetEndRegex = RegExp(r'{@end-tool}'); + + /// A RegExp that matches the start of a code block within dartdoc. + static final RegExp _codeBlockStartRegex = RegExp(r'///\s+```dart.*$'); + + /// A RegExp that matches the end of a code block within dartdoc. + static final RegExp _codeBlockEndRegex = RegExp(r'///\s+```\s*$'); + + /// A RegExp that matches a Dart constructor. + static final RegExp _constructorRegExp = RegExp(r'(const\s+)?_*[A-Z][a-zA-Z0-9<>._]*\('); + + /// A RegExp that matches a dart version specification in an example preamble. + static final RegExp _dartVersionRegExp = RegExp(r'\/\/ \/\/ @dart = ([0-9]+\.[0-9]+)'); + + /// Whether or not to print verbose output. + final bool verbose; + + /// Whether or not to keep the temp directory around after running. + /// + /// Defaults to false. + final bool _keepTmp; + + /// The temporary directory where all output is written. This will be deleted + /// automatically if there are no errors. + final Directory _tempDirectory; + + /// The package directory for the flutter package within the flutter root dir. + final Directory _flutterPackage; + + /// The directory for the dart:ui code to be analyzed with the flutter code. + /// + /// If this is null, then no dart:ui code is included in the analysis. It + /// defaults to the location inside of the flutter bin/cache directory that + /// contains the dart:ui code supplied by the engine. + final Directory? _dartUiLocation; + + /// A serial number so that we can create unique expression names when we + /// generate them. + int _expressionId = 0; + + static List _listDartFiles(Directory directory, {bool recursive = false}) { + return directory.listSync(recursive: recursive, followLinks: false).whereType().where((File file) => path.extension(file.path) == '.dart').toList(); + } + + /// Computes the headers needed for each snippet file. + List<_Line> get headers { + return _headers ??= [ + '// ignore_for_file: directives_ordering', + '// ignore_for_file: unnecessary_import', + '// ignore_for_file: unused_import', + '// ignore_for_file: unused_element', + '// ignore_for_file: unused_local_variable', + "import 'dart:async';", + "import 'dart:convert';", + "import 'dart:math' as math;", + "import 'dart:typed_data';", + "import 'dart:ui' as ui;", + "import 'package:flutter_test/flutter_test.dart';", + for (final File file in _listDartFiles(Directory(_defaultFlutterPackage))) + "import 'package:flutter/${path.basename(file.path)}';", + ].map<_Line>((String code) => _Line.generated(code: code, filename: 'headers')).toList(); + } + + List<_Line>? _headers; + + /// Checks all the snippets in the Dart files in [_flutterPackage] for errors. + Future checkSnippets() async { + _AnalysisResult? analysisResult; + try { + final Map sections = {}; + if (_dartUiLocation != null && !_dartUiLocation!.existsSync()) { + stderr.writeln('Unable to analyze engine dart snippets at ${_dartUiLocation!.path}.'); + } + final List filesToAnalyze = [ + ..._listDartFiles(_flutterPackage, recursive: true), + if (_dartUiLocation != null && _dartUiLocation!.existsSync()) ... _listDartFiles(_dartUiLocation!, recursive: true), + ]; + await _extractSnippets(filesToAnalyze, sectionMap: sections); + analysisResult = _analyze(_tempDirectory, sections); + } finally { + if (analysisResult != null && analysisResult.errors.isNotEmpty) { + for (final String filePath in analysisResult.errors.keys) { + analysisResult.errors[filePath]!.forEach(stderr.writeln); + } + stderr.writeln('\nFound ${analysisResult.errors.length} snippet code errors.'); + } + if (_keepTmp) { + print('Leaving temporary directory ${_tempDirectory.path} around for your perusal.'); + } else { + try { + _tempDirectory.deleteSync(recursive: true); + } on FileSystemException catch (e) { + stderr.writeln('Failed to delete ${_tempDirectory.path}: $e'); + } + } + } + return analysisResult.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.split(filename).join('.'); + snippetId = path.basenameWithoutExtension(snippetId); + snippetId = '$prefix.$snippetId.$start'; + return snippetId; + } + + /// Extracts the snippets from the Dart files in [files], writes them + /// to disk, and adds them to the [sectionMap]. + Future _extractSnippets( + List files, { + required Map sectionMap, + bool silent = false, + }) async { + final List<_Section> sections = <_Section>[]; + + for (final File file in files) { + final String relativeFilePath = path.relative(file.path, from: _flutterRoot); + final List snippetLine = file.readAsLinesSync(); + final List<_Section> preambleSections = <_Section>[]; + // Whether or not we're in the file-wide preamble section ("Examples can assume"). + bool inPreamble = false; + // Whether or not we're in a code snippet + bool inSnippetSection = false; + // Whether or not we're in a '```dart' segment. + bool inDart = false; + String? dartVersionOverride; + int lineNumber = 0; + final List block = []; + late _Line startLine; + for (final String line in snippetLine) { + lineNumber += 1; + final String trimmedLine = line.trim(); + if (inPreamble) { + if (line.isEmpty) { + inPreamble = false; + // If there's only a dartVersionOverride in the preamble, don't add + // it as a section. The dartVersionOverride was processed below. + if (dartVersionOverride == null || block.isNotEmpty) { + preambleSections.add(_processBlock(startLine, block)); + } + block.clear(); + } else if (!line.startsWith('// ')) { + throw _SnippetCheckerException('Unexpected content in snippet code preamble.', file: relativeFilePath, line: lineNumber); + } else if (_dartVersionRegExp.hasMatch(line)) { + dartVersionOverride = line.substring(3); + } else { + block.add(line.substring(3)); + } + } else if (inSnippetSection) { + if (_dartDocSnippetEndRegex.hasMatch(trimmedLine)) { + if (inDart) { + throw _SnippetCheckerException("Dart section didn't terminate before end of snippet", file: relativeFilePath, line: lineNumber); + } + inSnippetSection = false; + } + if (inDart) { + if (_codeBlockEndRegex.hasMatch(trimmedLine)) { + inDart = false; + final _Section processed = _processBlock(startLine, block); + final _Section combinedSection = preambleSections.isEmpty ? processed : _Section.combine(preambleSections..add(processed)); + sections.add(combinedSection.copyWith(dartVersionOverride: dartVersionOverride)); + block.clear(); + } else if (trimmedLine == _dartDocPrefix) { + block.add(''); + } else { + final int index = line.indexOf(_dartDocPrefixWithSpace); + if (index < 0) { + throw _SnippetCheckerException( + 'Dart section inexplicably did not contain "$_dartDocPrefixWithSpace" prefix.', + file: relativeFilePath, + line: lineNumber, + ); + } + block.add(line.substring(index + 4)); + } + } else if (_codeBlockStartRegex.hasMatch(trimmedLine)) { + assert(block.isEmpty); + startLine = _Line( + filename: relativeFilePath, + line: lineNumber + 1, + indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length, + ); + inDart = true; + } + } + if (!inSnippetSection) { + final RegExpMatch? snippetMatch = _dartDocSnippetBeginRegex.firstMatch(trimmedLine); + if (line == '// Examples can assume:') { + assert(block.isEmpty); + startLine = _Line.generated(filename: relativeFilePath, line: lineNumber + 1, indent: 3); + inPreamble = true; + } else if (snippetMatch != null) { + inSnippetSection = true; + } else if (RegExp(r'///\s*#+\s+[Ss]ample\s+[Cc]ode:?$').hasMatch(trimmedLine)) { + throw _SnippetCheckerException( + "Found deprecated '## Sample code' section: use {@tool snippet}...{@end-tool} instead.", + file: relativeFilePath, + line: lineNumber, + ); + } + } + } + } + if (!silent) + print('Found ${sections.length} snippet code blocks'); + for (final _Section section in sections) { + final String path = _writeSection(section).path; + if (sectionMap != null) + sectionMap[path] = section; + } + } + + /// Creates the configuration files necessary for the analyzer to consider + /// the temporary directory a package, and sets which lint rules to enforce. + void _createConfigurationFiles(Directory directory) { + final File targetPubSpec = File(path.join(directory.path, 'pubspec.yaml')); + if (!targetPubSpec.existsSync()) { + // Copying pubspec.yaml from examples/api into temp directory. + final File sourcePubSpec = File(path.join(_flutterRoot, 'examples', 'api', 'pubspec.yaml')); + if (!sourcePubSpec.existsSync()) { + throw 'Cannot find pubspec.yaml at ${sourcePubSpec.path}, which is also used to analyze code snippets.'; + } + sourcePubSpec.copySync(targetPubSpec.path); + } + final File targetAnalysisOptions = File(path.join(directory.path, 'analysis_options.yaml')); + if (!targetAnalysisOptions.existsSync()) { + // Use the same analysis_options.yaml configuration that's used for examples/api. + final File sourceAnalysisOptions = File(path.join(_flutterRoot, 'examples', 'api', 'analysis_options.yaml')); + if (!sourceAnalysisOptions.existsSync()) { + throw 'Cannot find analysis_options.yaml at ${sourceAnalysisOptions.path}, which is also used to analyze code snippets.'; + } + targetAnalysisOptions + ..createSync(recursive: true) + ..writeAsStringSync('include: ${sourceAnalysisOptions.absolute.path}'); + } + } + + /// Writes out a snippet section to the disk and returns the file. + File _writeSection(_Section section) { + 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 List<_Line> mainContents = <_Line>[ + _Line.generated(code: section.dartVersionOverride ?? '', filename: section.start.filename), + ...headers, + _Line.generated(filename: section.start.filename), + _Line.generated(code: '// From: ${section.start.filename}:${section.start.line}', filename: section.start.filename), + ...section.code, + _Line.generated(filename: section.start.filename), // empty line at EOF + ]; + outputFile.writeAsStringSync(mainContents.map((_Line line) => line.code).join('\n')); + return outputFile; + } + + /// Invokes the analyzer on the given [directory] and returns the stdout. + int _runAnalyzer(Directory directory, {bool silent = true, required List output}) { + if (!silent) + print('Starting analysis of code snippets.'); + _createConfigurationFiles(directory); + final ProcessResult result = Process.runSync( + _flutter, + ['--no-wrap', 'analyze', '--no-preamble', '--no-congratulate', '.'], + workingDirectory: directory.absolute.path, + ); + final List stderr = result.stderr.toString().trim().split('\n'); + final List stdout = result.stdout.toString().trim().split('\n'); + // Remove output from building the flutter tool. + stderr.removeWhere((String line) { + return line.startsWith('Building flutter tool...') + || line.startsWith('Waiting for another flutter command to release the startup lock...'); + }); + // Check out the stderr to see if the analyzer had it's own issues. + if (stderr.isNotEmpty && stderr.first.contains(RegExp(r' issues? found\. \(ran in '))) { + stderr.removeAt(0); + if (stderr.isNotEmpty && stderr.last.isEmpty) { + stderr.removeLast(); + } + } + if (stderr.isNotEmpty && stderr.any((String line) => line.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 pub get" in ')) { + stdout.removeAt(0); + } + output.addAll(stdout); + return result.exitCode; + } + + /// 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. + _AnalysisResult _analyze( + Directory directory, + Map sections, { + bool silent = false, + }) { + final List errors = []; + int exitCode = _runAnalyzer(directory, silent: silent, output: errors); + + final Map> analysisErrors = >{}; + 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 ? ' - ' : ' • '; + // 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 + 3; + for (final String error in errors) { + final RegExpMatch? match = errorPattern.firstMatch(error); + if (match == null) { + stderr.writeln('Analyzer output: $error'); + unknownAnalyzerErrors = true; + continue; + } + final String type = match.namedGroup('type')!; + final String message = match.namedGroup('description')!; + final File file = File(path.join(_tempDirectory.path, match.namedGroup('file'))); + final List fileContents = file.readAsLinesSync(); + final String lineString = match.namedGroup('line')!; + final String columnString = match.namedGroup('column')!; + final String errorCode = match.namedGroup('code')!; + final int lineNumber = int.parse(lineString, radix: 10) - headerLength; + final int columnNumber = int.parse(columnString, radix: 10); + + if (lineNumber < 1 || lineNumber > fileContents.length) { + addAnalysisError( + file, + _AnalysisError( + type, + lineNumber, + columnNumber, + message, + errorCode, + _Line(filename: file.path, line: lineNumber), + ), + ); + throw _SnippetCheckerException('Failed to parse error message: $error', file: file.path, line: lineNumber); + } + + 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, + 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 snippet code.'; + source = previousCodeLine; + } + } + } else { + line = actualLine.line; + column = actualLine.indent + columnNumber; + } + addAnalysisError( + file, + _AnalysisError( + type, + line, + column, + errorMessage, + errorCode, + source, + ), + ); + } + if (exitCode == 1 && analysisErrors.isEmpty && !unknownAnalyzerErrors) { + exitCode = 0; + } + if (exitCode == 0) { + if (!silent) + print('No analysis errors in snippets!'); + assert(analysisErrors.isEmpty); + } + return _AnalysisResult(exitCode, analysisErrors); + } + + /// Process one block of snippet code (the part inside of "```" markers). + /// Splits any sections denoted by "// ..." into separate blocks to be + /// processed separately. Uses a primitive heuristic to make snippet blocks + /// into valid Dart code. + _Section _processBlock(_Line line, List block) { + if (block.isEmpty) { + throw _SnippetCheckerException('$line: Empty ```dart block in snippet code.'); + } + if (block.first.startsWith('new ') || block.first.startsWith(_constructorRegExp)) { + _expressionId += 1; + return _Section.surround(line, 'dynamic expression$_expressionId = ', block.toList(), ';'); + } else if (block.first.startsWith('await ')) { + _expressionId += 1; + return _Section.surround(line, 'Future expression$_expressionId() async { ', block.toList(), ' }'); + } else if (block.first.startsWith('class ') || block.first.startsWith('enum ')) { + return _Section.fromStrings(line, block.toList()); + } else if ((block.first.startsWith('_') || block.first.startsWith('final ')) && block.first.contains(' = ')) { + _expressionId += 1; + return _Section.surround(line, 'void expression$_expressionId() { ', block.toList(), ' }'); + } else { + final List buffer = []; + 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 _SnippetCheckerException('${_Line(filename: line.filename, line: line.line + index, indent: line.indent)}: ' + 'Unexpected blank line or "// ..." line near start of subblock in snippet code.'); + subblocks += 1; + 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( + code: block[index], + filename: line.filename, + line: line.line + index, + indent: line.indent, + ); + buffer.add(block[index]); + } + } + if (subblocks > 0) { + 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 { + return _Section.fromStrings(line, block.toList()); + } + } + } +} + +/// A class to represent a line of input code. +class _Line { + const _Line({this.code = '', required this.filename, this.line = -1, this.indent = 0}) + : generated = false; + const _Line.generated({this.code = '', required this.filename, this.line = -1, this.indent = 0}) + : generated = true; + + /// The file that this line came from, or the file that the line was generated for, if [generated] is true. + final String filename; + final int line; + final int indent; + final String code; + final bool generated; + + String toStringWithColumn(int column) { + if (column != null && indent != null) { + return '$filename:$line:${column + indent}: $code'; + } + return toString(); + } + + @override + String toString() => '$filename:$line: $code'; +} + +/// A class to represent a section of snippet code, marked by "{@tool snippet}...{@end-tool}". +class _Section { + const _Section(this.code, {this.dartVersionOverride}); + factory _Section.combine(List<_Section> sections) { + final List<_Line> code = sections + .expand((_Section section) => section.code) + .toList(); + return _Section(code); + } + factory _Section.fromStrings(_Line firstLine, List code) { + final List<_Line> codeLines = <_Line>[]; + for (int i = 0; i < code.length; ++i) { + codeLines.add( + _Line( + code: code[i], + filename: firstLine.filename, + line: firstLine.line + i, + indent: firstLine.indent, + ), + ); + } + return _Section(codeLines); + } + factory _Section.surround(_Line firstLine, String prefix, List 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: code[i], + filename: firstLine.filename, + line: firstLine.line + i, + indent: firstLine.indent, + ), + ); + } + return _Section(<_Line>[ + _Line.generated(code: prefix, filename: firstLine.filename, line: 0), + ...codeLines, + _Line.generated(code: postfix, filename: firstLine.filename, line: 0), + ]); + } + _Line get start => code.firstWhere((_Line line) => !line.generated); + final List<_Line> code; + final String? dartVersionOverride; + + _Section copyWith({String? dartVersionOverride}) { + return _Section(code, dartVersionOverride: dartVersionOverride ?? this.dartVersionOverride); + } +} + +/// 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.type, + this.line, + this.column, + this.message, + this.errorCode, + this.source, + ); + + final String type; + final int line; + final int column; + final String message; + final String errorCode; + final _Line? source; + + @override + String toString() { + if (source != null) { + return '${source!.toStringWithColumn(column)}\n>>> $type: $message ($errorCode)'; + } else { + return ':$line:$column\n>>> $type: $message ($errorCode)'; + } + } +} + +Future _runInteractive({ + required Directory? tempDir, + required Directory flutterPackage, + required String filePath, + required Directory? dartUiLocation, +}) async { + filePath = path.isAbsolute(filePath) ? filePath : path.join(path.current, filePath); + final File file = File(filePath); + if (!file.existsSync()) { + throw 'Path ${file.absolute.path} does not exist ($filePath).'; + } + if (!path.isWithin(_flutterRoot, file.absolute.path) && + (dartUiLocation == null || !path.isWithin(dartUiLocation.path, file.absolute.path))) { + throw 'Path ${file.absolute.path} is not within the flutter root: ' + '$_flutterRoot${dartUiLocation != null ? ' or the dart:ui location: $dartUiLocation' : ''}'; + } + + if (tempDir == null) { + tempDir = Directory.systemTemp.createTempSync('flutter_analyze_snippet_code.'); + ProcessSignal.sigint.watch().listen((_) { + print('Deleting temp files...'); + tempDir!.deleteSync(recursive: true); + exit(0); + }); + print('Using temp dir ${tempDir.path}'); + } + print('Starting up in interactive mode on ${path.relative(filePath, from: _flutterRoot)} ...'); + + Future analyze(_SnippetChecker checker, File file) async { + final Map sections = {}; + await checker._extractSnippets([file], silent: true, sectionMap: sections); + final _AnalysisResult analysisResult = checker._analyze(checker._tempDirectory, sections, silent: true); + stderr.writeln('\u001B[2J\u001B[H'); // Clears the old results from the terminal. + if (analysisResult.errors.isNotEmpty) { + for (final String filePath in analysisResult.errors.keys) { + analysisResult.errors[filePath]!.forEach(stderr.writeln); + } + stderr.writeln('\nFound ${analysisResult.errors.length} errors.'); + } else { + stderr.writeln('\nNo issues found.'); + } + } + + final _SnippetChecker checker = _SnippetChecker(flutterPackage, tempDirectory: tempDir) + .._createConfigurationFiles(tempDir); + await analyze(checker, file); + + print('Type "q" to quit, or "r" to delete temp dir and manually reload.'); + + void rerun() { + print('\n\nRerunning...'); + try { + analyze(checker, file); + } on _SnippetCheckerException catch (e) { + print('Caught Exception (${e.runtimeType}), press "r" to retry:\n$e'); + } + } + + stdin.lineMode = false; + stdin.echoMode = false; + stdin.transform(utf8.decoder).listen((String input) { + switch (input) { + case 'q': + print('Exiting...'); + exit(0); + case 'r': + print('Deleting temp files...'); + tempDir!.deleteSync(recursive: true); + rerun(); + break; + } + }); + + Watcher(file.absolute.path).events.listen((_) => rerun()); +} diff --git a/dev/bots/docs.sh b/dev/bots/docs.sh index 3312402aabb..334eb017dbe 100755 --- a/dev/bots/docs.sh +++ b/dev/bots/docs.sh @@ -25,7 +25,6 @@ function generate_docs() { # Install and activate the snippets tool, which resides in the # assets-for-api-docs repo: # 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 # This script generates a unified doc set, and creates diff --git a/dev/bots/test/analyze-sample-code-test-dart-ui/ui.dart b/dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart similarity index 93% rename from dev/bots/test/analyze-sample-code-test-dart-ui/ui.dart rename to dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart index 5b0239d16df..e206228b0e3 100644 --- a/dev/bots/test/analyze-sample-code-test-dart-ui/ui.dart +++ b/dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart @@ -11,7 +11,7 @@ library dart.ui; /// Annotation used by Flutter's Dart compiler to indicate that an /// [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. /// /// ```dart diff --git a/dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart b/dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart similarity index 94% rename from dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart rename to dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart index c49252a86bd..ffe82f05ef8 100644 --- a/dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart +++ b/dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // 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. // Examples can assume: @@ -34,7 +34,7 @@ /// ``` /// {@end-tool} /// -/// {@tool dartpad --template=stateless_widget_material} +/// {@tool snippet} /// Bla blabla blabla some [Text] when the `_blabla` blabla blabla is true, and /// blabla it when it is blabla: /// @@ -121,7 +121,7 @@ /// ``` /// {@end-tool} /// -/// {@tool dartpad --template=stateless_widget_material} +/// {@tool snippet} /// Dartpad with null-safe syntax /// /// ```dart preamble diff --git a/dev/bots/test/analyze_sample_code_test.dart b/dev/bots/test/analyze_sample_code_test.dart deleted file mode 100644 index ac435ab14b8..00000000000 --- a/dev/bots/test/analyze_sample_code_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -import 'common.dart'; - -void main() { - // These tests don't run on Windows because the sample analyzer doesn't - // support Windows as a platform, since it is only run on Linux in the - // continuous integration tests. - if (Platform.isWindows) { - return; - } - - test('analyze_sample_code smoke test', () { - final ProcessResult process = Process.runSync( - '../../bin/cache/dart-sdk/bin/dart', - ['analyze_sample_code.dart', '--no-include-dart-ui', 'test/analyze-sample-code-test-input'], - ); - final List stdoutLines = process.stdout.toString().split('\n'); - final List stderrLines = process.stderr.toString().split('\n'); - expect(process.exitCode, isNot(equals(0))); - expect(stderrLines, containsAll([ - 'In sample starting at dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:125: 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\)")), - 'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:30:9: new Opacity(', - '>>> info: Unnecessary new keyword (unnecessary_new)', - 'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:62:9: new Opacity(', - '>>> info: Unnecessary new keyword (unnecessary_new)', - "dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:111:9: final String? bar = 'Hello';", - '>>> 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;', - '>>> 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;', - ">>> 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(),', - '>>> error: Unexpected comma at end of sample code. (missing_identifier)', - 'Found 2 sample code errors.', - ])); - expect(stdoutLines, containsAll([ - 'Found 9 snippet code blocks, 0 sample code sections, and 2 dartpad sections.', - 'Starting analysis of code samples.', - ])); - }); - test('Analyzes dart:ui code', () { - final ProcessResult process = Process.runSync( - '../../bin/cache/dart-sdk/bin/dart', - [ - 'analyze_sample_code.dart', - '--dart-ui-location=test/analyze-sample-code-test-dart-ui', - 'test/analyze-sample-code-test-input', - ], - ); - final List stdoutLines = process.stdout.toString().split('\n'); - final List stderrLines = process.stderr.toString().split('\n'); - expect(process.exitCode, isNot(equals(0))); - expect(stderrLines, containsAll([ - 'In sample starting at dev/bots/test/analyze-sample-code-test-dart-ui/ui.dart:15:class MyStatelessWidget extends StatelessWidget {', - ">>> error: Missing concrete implementation of 'StatelessWidget.build' (non_abstract_class_inherits_abstract_member)", - 'In sample starting at dev/bots/test/analyze-sample-code-test-dart-ui/ui.dart:15:class MyStringBuffer {', - ">>> error: Classes can't be declared inside other classes (class_in_class)", - ])); - expect(stdoutLines, containsAll([ - // There is one sample code section in the test's dummy dart:ui code. - 'Found 9 snippet code blocks, 1 sample code sections, and 2 dartpad sections.', - 'Starting analysis of code samples.', - ])); - }); -} diff --git a/dev/bots/test/analyze_snippet_code_test.dart b/dev/bots/test/analyze_snippet_code_test.dart new file mode 100644 index 00000000000..c5fadf161f0 --- /dev/null +++ b/dev/bots/test/analyze_snippet_code_test.dart @@ -0,0 +1,71 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'common.dart'; + +void main() { + // These tests don't run on Windows because the sample analyzer doesn't + // support Windows as a platform, since it is only run on Linux in the + // continuous integration tests. + if (Platform.isWindows) { + return; + } + + test('analyze_snippet_code smoke test', () { + final ProcessResult process = Process.runSync( + '../../bin/cache/dart-sdk/bin/dart', + ['analyze_snippet_code.dart', '--no-include-dart-ui', 'test/analyze-snippet-code-test-input'], + ); + final List stdoutLines = process.stdout.toString().split('\n'); + final List stderrLines = process.stderr.toString().split('\n'); + expect(process.exitCode, isNot(equals(0))); + expect(stderrLines, containsAll([ + '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\)")), + 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:30:9: new Opacity(', + '>>> info: Unnecessary new keyword (unnecessary_new)', + 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:62:9: new Opacity(', + '>>> info: Unnecessary new keyword (unnecessary_new)', + "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)', + '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)', + '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)", + 'dev/bots/test/analyze-snippet-code-test-input/known_broken_documentation.dart:120:24: const SizedBox(),', + '>>> error: Unexpected comma at end of snippet code. (missing_identifier)', + 'Found 1 snippet code errors.', + ])); + expect(stdoutLines, containsAll([ + 'Found 13 snippet code blocks', + 'Starting analysis of code snippets.', + ])); + }); + test('Analyzes dart:ui code', () { + final ProcessResult process = Process.runSync( + '../../bin/cache/dart-sdk/bin/dart', + [ + 'analyze_snippet_code.dart', + '--dart-ui-location=test/analyze-snippet-code-test-dart-ui', + 'test/analyze-snippet-code-test-input', + ], + ); + final List stdoutLines = process.stdout.toString().split('\n'); + final List stderrLines = process.stderr.toString().split('\n'); + expect(process.exitCode, isNot(equals(0))); + expect(stderrLines, containsAll([ + 'dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart:19:11: error;', + ">>> error: Variables must be declared using the keywords 'const', 'final', 'var' or a type name (missing_const_final_var_or_type)", + 'dev/bots/test/analyze-snippet-code-test-dart-ui/ui.dart:23:11: @keepToString', + ">>> error: Undefined name 'keepToString' used as an annotation (undefined_annotation)", + ])); + expect(stdoutLines, containsAll([ + // There is one snippet code section in the test's dummy dart:ui code. + 'Found 14 snippet code blocks', + 'Starting analysis of code snippets.', + ])); + }); +} diff --git a/examples/api/analysis_options.yaml b/examples/api/analysis_options.yaml index 79ac82d286b..7e59ed2a793 100644 --- a/examples/api/analysis_options.yaml +++ b/examples/api/analysis_options.yaml @@ -1,7 +1,11 @@ +# This file is also used by dev/bots/analyze_snippet_code.dart to analyze code snippets (`{@tool snippet}` sections). + include: ../../analysis_options.yaml linter: rules: + # Samples want to print things pretty often. + avoid_print: false # TODO(goderbauer): enable when super params are more established and # seeing them in the API samples is no longer surprising, # https://github.com/flutter/flutter/issues/101068 diff --git a/examples/api/pubspec.yaml b/examples/api/pubspec.yaml index 444a06b2f22..dd00b842a84 100644 --- a/examples/api/pubspec.yaml +++ b/examples/api/pubspec.yaml @@ -1,3 +1,5 @@ +# This file is also used by dev/bots/analyze_snippet_code.dart to analyze code snippets (`{@tool snippet}` sections). + name: flutter_api_samples description: API code samples for the Flutter repo. publish_to: 'none' diff --git a/packages/flutter/lib/src/foundation/diagnostics.dart b/packages/flutter/lib/src/foundation/diagnostics.dart index 286ec17250e..dc77398c84b 100644 --- a/packages/flutter/lib/src/foundation/diagnostics.dart +++ b/packages/flutter/lib/src/foundation/diagnostics.dart @@ -3213,7 +3213,6 @@ mixin Diagnosticable { /// value: isCurrent, /// ifTrue: 'active', /// ifFalse: 'inactive', - /// showName: false, /// )); /// /// properties.add(DiagnosticsProperty('keepAlive', keepAlive)); diff --git a/packages/flutter/lib/src/material/checkbox.dart b/packages/flutter/lib/src/material/checkbox.dart index 05442f2ec81..ca20f5abbeb 100644 --- a/packages/flutter/lib/src/material/checkbox.dart +++ b/packages/flutter/lib/src/material/checkbox.dart @@ -171,7 +171,7 @@ class Checkbox extends StatefulWidget { /// Checkbox( /// value: true, /// onChanged: (_){}, - /// fillColor: MaterialStateProperty.resolveWith((states) { + /// fillColor: MaterialStateProperty.resolveWith((Set states) { /// if (states.contains(MaterialState.disabled)) { /// return Colors.orange.withOpacity(.32); /// } diff --git a/packages/flutter/lib/src/material/radio.dart b/packages/flutter/lib/src/material/radio.dart index 79714204f0b..7daff69b5ed 100644 --- a/packages/flutter/lib/src/material/radio.dart +++ b/packages/flutter/lib/src/material/radio.dart @@ -200,7 +200,7 @@ class Radio extends StatefulWidget { /// value: 1, /// groupValue: 1, /// onChanged: (_){}, - /// fillColor: MaterialStateProperty.resolveWith((states) { + /// fillColor: MaterialStateProperty.resolveWith((Set states) { /// if (states.contains(MaterialState.disabled)) { /// return Colors.orange.withOpacity(.32); /// } diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart index 9d5c04248b4..f0ca58c3adc 100644 --- a/packages/flutter/lib/src/material/switch.dart +++ b/packages/flutter/lib/src/material/switch.dart @@ -238,7 +238,7 @@ class Switch extends StatelessWidget { /// Switch( /// value: true, /// onChanged: (_) => true, - /// thumbColor: MaterialStateProperty.resolveWith((states) { + /// thumbColor: MaterialStateProperty.resolveWith((Set states) { /// if (states.contains(MaterialState.disabled)) { /// return Colors.orange.withOpacity(.48); /// } @@ -279,7 +279,7 @@ class Switch extends StatelessWidget { /// Switch( /// value: true, /// onChanged: (_) => true, - /// thumbColor: MaterialStateProperty.resolveWith((states) { + /// thumbColor: MaterialStateProperty.resolveWith((Set states) { /// if (states.contains(MaterialState.disabled)) { /// return Colors.orange.withOpacity(.48); /// } diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index e50358d9386..b9694c113ab 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -201,9 +201,7 @@ enum MaterialTapTargetSize { /// ```dart /// MaterialApp( /// theme: ThemeData( -/// colorScheme: ColorScheme.fromSwatch( -/// primarySwatch: Colors.blue, -/// ).copyWith( +/// colorScheme: ColorScheme.fromSwatch().copyWith( /// secondary: Colors.green, /// ), /// textTheme: const TextTheme(bodyText2: TextStyle(color: Colors.purple)), diff --git a/packages/flutter/lib/src/painting/box_border.dart b/packages/flutter/lib/src/painting/box_border.dart index 9819e1d6589..e10058ba61a 100644 --- a/packages/flutter/lib/src/painting/box_border.dart +++ b/packages/flutter/lib/src/painting/box_border.dart @@ -267,20 +267,20 @@ abstract class BoxBorder extends ShapeBorder { /// Container( /// decoration: const BoxDecoration( /// border: Border( -/// top: BorderSide(width: 1.0, color: Color(0xFFFFFFFF)), -/// left: BorderSide(width: 1.0, color: Color(0xFFFFFFFF)), -/// right: BorderSide(width: 1.0, color: Color(0xFF000000)), -/// bottom: BorderSide(width: 1.0, color: Color(0xFF000000)), +/// top: BorderSide(color: Color(0xFFFFFFFF)), +/// left: BorderSide(color: Color(0xFFFFFFFF)), +/// right: BorderSide(), +/// bottom: BorderSide(), /// ), /// ), /// child: Container( /// padding: const EdgeInsets.symmetric(horizontal: 20.0, vertical: 2.0), /// decoration: const BoxDecoration( /// border: Border( -/// top: BorderSide(width: 1.0, color: Color(0xFFDFDFDF)), -/// left: BorderSide(width: 1.0, color: Color(0xFFDFDFDF)), -/// right: BorderSide(width: 1.0, color: Color(0xFF7F7F7F)), -/// bottom: BorderSide(width: 1.0, color: Color(0xFF7F7F7F)), +/// top: BorderSide(color: Color(0xFFDFDFDF)), +/// left: BorderSide(color: Color(0xFFDFDFDF)), +/// right: BorderSide(color: Color(0xFF7F7F7F)), +/// bottom: BorderSide(color: Color(0xFF7F7F7F)), /// ), /// color: Color(0xFFBFBFBF), /// ), diff --git a/packages/flutter/lib/src/painting/box_decoration.dart b/packages/flutter/lib/src/painting/box_decoration.dart index 86e17df3d8d..b68717abf15 100644 --- a/packages/flutter/lib/src/painting/box_decoration.dart +++ b/packages/flutter/lib/src/painting/box_decoration.dart @@ -49,7 +49,6 @@ import 'image_provider.dart'; /// fit: BoxFit.cover, /// ), /// border: Border.all( -/// color: Colors.black, /// width: 8, /// ), /// borderRadius: BorderRadius.circular(12), diff --git a/packages/flutter/lib/src/painting/gradient.dart b/packages/flutter/lib/src/painting/gradient.dart index b37e3e45c99..976291dd40d 100644 --- a/packages/flutter/lib/src/painting/gradient.dart +++ b/packages/flutter/lib/src/painting/gradient.dart @@ -857,8 +857,6 @@ class RadialGradient extends Gradient { /// decoration: const BoxDecoration( /// gradient: SweepGradient( /// center: FractionalOffset.center, -/// startAngle: 0.0, -/// endAngle: math.pi * 2, /// colors: [ /// Color(0xFF4285F4), // blue /// Color(0xFF34A853), // green @@ -883,8 +881,6 @@ class RadialGradient extends Gradient { /// decoration: const BoxDecoration( /// gradient: SweepGradient( /// center: FractionalOffset.center, -/// startAngle: 0.0, -/// endAngle: math.pi * 2, /// colors: [ /// Color(0xFF4285F4), // blue /// Color(0xFF34A853), // green diff --git a/packages/flutter/lib/src/rendering/layer.dart b/packages/flutter/lib/src/rendering/layer.dart index b7f468acfaf..0eca6b41dfa 100644 --- a/packages/flutter/lib/src/rendering/layer.dart +++ b/packages/flutter/lib/src/rendering/layer.dart @@ -118,7 +118,6 @@ class AnnotationResult { /// offset, /// Offset.zero & size, /// super.paint, -/// clipBehavior: Clip.hardEdge, /// oldLayer: _clipRectLayer.layer, /// ); /// } diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index a7373e589d1..2bdda8b81c3 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -2098,7 +2098,6 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { /// ```dart /// PhysicalModel( // A /// color: Colors.amber, - /// elevation: 0.0, /// child: Semantics( /// explicitChildNodes: true, /// child: const PhysicalModel( // B diff --git a/packages/flutter/lib/src/services/platform_channel.dart b/packages/flutter/lib/src/services/platform_channel.dart index 072a6c8f06c..246391a1dd5 100644 --- a/packages/flutter/lib/src/services/platform_channel.dart +++ b/packages/flutter/lib/src/services/platform_channel.dart @@ -198,6 +198,9 @@ class MethodChannel { /// /// ```dart /// class Music { + /// // Class cannot be instantiated. + /// const Music._(); + /// /// static const MethodChannel _channel = MethodChannel('music'); /// /// static Future isLicensed() async { @@ -213,7 +216,7 @@ class MethodChannel { /// // the actual values involved would support such a typed container. /// // The correct type cannot be inferred with any value of `T`. /// final List? songs = await _channel.invokeMethod>('getSongs'); - /// return songs?.map(Song.fromJson).toList() ?? []; + /// return songs?.cast>().map(Song.fromJson).toList() ?? []; /// } /// /// static Future play(Song song, double volume) async { @@ -225,7 +228,7 @@ class MethodChannel { /// 'volume': volume, /// }); /// } 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 { /// final String title; /// final String artist; /// - /// static Song fromJson(dynamic json) { - /// return Song(json['id'] as String, json['title'] as String, json['artist'] as String); + /// static Song fromJson(Map json) { + /// return Song(json['id']! as String, json['title']! as String, json['artist']! as String); /// } /// } /// ``` diff --git a/packages/flutter/lib/src/widgets/animated_cross_fade.dart b/packages/flutter/lib/src/widgets/animated_cross_fade.dart index 78559a987ff..3602c9ddd7c 100644 --- a/packages/flutter/lib/src/widgets/animated_cross_fade.dart +++ b/packages/flutter/lib/src/widgets/animated_cross_fade.dart @@ -44,7 +44,6 @@ enum CrossFadeState { /// ```dart /// Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) { /// return Stack( -/// fit: StackFit.loose, /// children: [ /// Positioned( /// key: bottomChildKey, diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 0e1bf2adcf9..c07bbb96ec3 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -4566,7 +4566,6 @@ class Flex extends MultiChildRenderObjectWidget { /// ), /// Expanded( /// child: FittedBox( -/// fit: BoxFit.contain, // otherwise the logo will be tiny /// child: FlutterLogo(), /// ), /// ), @@ -4765,7 +4764,6 @@ class Row extends Flex { /// Text('Craft beautiful UIs'), /// Expanded( /// child: FittedBox( -/// fit: BoxFit.contain, // otherwise the logo will be tiny /// child: FlutterLogo(), /// ), /// ), diff --git a/packages/flutter/lib/src/widgets/localizations.dart b/packages/flutter/lib/src/widgets/localizations.dart index 71643b9a70d..258f70ac2be 100644 --- a/packages/flutter/lib/src/widgets/localizations.dart +++ b/packages/flutter/lib/src/widgets/localizations.dart @@ -11,7 +11,7 @@ import 'debug.dart'; import 'framework.dart'; // 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 initializeMessages(String locale) => Future.value(); // Used by loadAll() to record LocalizationsDelegate.load() futures we're diff --git a/packages/flutter/lib/src/widgets/page_view.dart b/packages/flutter/lib/src/widgets/page_view.dart index ef75f9af0ab..8e8400b96e0 100644 --- a/packages/flutter/lib/src/widgets/page_view.dart +++ b/packages/flutter/lib/src/widgets/page_view.dart @@ -108,7 +108,6 @@ import 'viewport.dart'; /// ); /// } /// } -/// /// ``` /// {@end-tool} class PageController extends ScrollController { diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index 51040798d3c..99fa2ea35fd 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -27,7 +27,7 @@ import 'scroll_controller.dart'; import 'transitions.dart'; // Examples can assume: -// dynamic routeObserver; +// late RouteObserver> routeObserver; // late NavigatorState navigator; // late BuildContext context; // Future askTheUserIfTheyAreSure() async { return true; } @@ -1058,7 +1058,7 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute( /// pageBuilder: (BuildContext context, /// Animation animation, /// Animation secondaryAnimation, @@ -1105,7 +1105,7 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute( /// pageBuilder: (BuildContext context, /// Animation animation, /// Animation secondaryAnimation,