mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Avoid analyzing API example code twice, clean-up (#103548)
This commit is contained in:
parent
2b19ecdc27
commit
8bec125aaf
@ -171,10 +171,10 @@ Future<void> run(List<String> 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,
|
||||
<String>[path.join(flutterRoot, 'dev', 'bots', 'analyze_sample_code.dart'), '--verbose'],
|
||||
<String>[path.join(flutterRoot, 'dev', 'bots', 'analyze_snippet_code.dart'), '--verbose'],
|
||||
workingDirectory: flutterRoot,
|
||||
);
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
850
dev/bots/analyze_snippet_code.dart
Normal file
850
dev/bots/analyze_snippet_code.dart
Normal file
@ -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<void> main(List<String> 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<String, List<_AnalysisError>> 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. "<flutter repo>/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<File> _listDartFiles(Directory directory, {bool recursive = false}) {
|
||||
return directory.listSync(recursive: recursive, followLinks: false).whereType<File>().where((File file) => path.extension(file.path) == '.dart').toList();
|
||||
}
|
||||
|
||||
/// Computes the headers needed for each snippet file.
|
||||
List<_Line> get headers {
|
||||
return _headers ??= <String>[
|
||||
'// 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<int> checkSnippets() async {
|
||||
_AnalysisResult? analysisResult;
|
||||
try {
|
||||
final Map<String, _Section> sections = <String, _Section>{};
|
||||
if (_dartUiLocation != null && !_dartUiLocation!.existsSync()) {
|
||||
stderr.writeln('Unable to analyze engine dart snippets at ${_dartUiLocation!.path}.');
|
||||
}
|
||||
final List<File> filesToAnalyze = <File>[
|
||||
..._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<void> _extractSnippets(
|
||||
List<File> files, {
|
||||
required Map<String, _Section> 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<String> 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<String> block = <String>[];
|
||||
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<String>((_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<String> output}) {
|
||||
if (!silent)
|
||||
print('Starting analysis of code snippets.');
|
||||
_createConfigurationFiles(directory);
|
||||
final ProcessResult result = Process.runSync(
|
||||
_flutter,
|
||||
<String>['--no-wrap', 'analyze', '--no-preamble', '--no-congratulate', '.'],
|
||||
workingDirectory: directory.absolute.path,
|
||||
);
|
||||
final List<String> stderr = result.stderr.toString().trim().split('\n');
|
||||
final List<String> 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<String, _Section> sections, {
|
||||
bool silent = false,
|
||||
}) {
|
||||
final List<String> errors = <String>[];
|
||||
int exitCode = _runAnalyzer(directory, silent: silent, output: errors);
|
||||
|
||||
final Map<String, List<_AnalysisError>> analysisErrors = <String, List<_AnalysisError>>{};
|
||||
void addAnalysisError(File file, _AnalysisError error) {
|
||||
if (analysisErrors.containsKey(file.path)) {
|
||||
analysisErrors[file.path]!.add(error);
|
||||
} else {
|
||||
analysisErrors[file.path] = <_AnalysisError>[error];
|
||||
}
|
||||
}
|
||||
|
||||
final String kBullet = Platform.isWindows ? ' - ' : ' • ';
|
||||
// RegExp to match an error output line of the analyzer.
|
||||
final RegExp errorPattern = RegExp(
|
||||
'^ +(?<type>[a-z]+)'
|
||||
'$kBullet(?<description>.+)'
|
||||
'$kBullet(?<file>.+):(?<line>[0-9]+):(?<column>[0-9]+)'
|
||||
'$kBullet(?<code>[-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<String> 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<String> 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<void> 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<String> buffer = <String>[];
|
||||
int subblocks = 0;
|
||||
_Line? subline;
|
||||
final List<_Section> subsections = <_Section>[];
|
||||
for (int index = 0; index < block.length; index += 1) {
|
||||
// Each section of the dart code that is either split by a blank line, or with '// ...' is
|
||||
// treated as a separate code block.
|
||||
if (block[index] == '' || block[index] == '// ...') {
|
||||
if (subline == null)
|
||||
throw _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<String> 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<String> code, String postfix) {
|
||||
assert(prefix != null);
|
||||
assert(postfix != null);
|
||||
final List<_Line> codeLines = <_Line>[];
|
||||
for (int i = 0; i < code.length; ++i) {
|
||||
codeLines.add(
|
||||
_Line(
|
||||
code: 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 '<source unknown>:$line:$column\n>>> $type: $message ($errorCode)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _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<void> analyze(_SnippetChecker checker, File file) async {
|
||||
final Map<String, _Section> sections = <String, _Section>{};
|
||||
await checker._extractSnippets(<File>[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());
|
||||
}
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -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',
|
||||
<String>['analyze_sample_code.dart', '--no-include-dart-ui', 'test/analyze-sample-code-test-input'],
|
||||
);
|
||||
final List<String> stdoutLines = process.stdout.toString().split('\n');
|
||||
final List<String> stderrLines = process.stderr.toString().split('\n');
|
||||
expect(process.exitCode, isNot(equals(0)));
|
||||
expect(stderrLines, containsAll(<Object>[
|
||||
'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(<String>[
|
||||
'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',
|
||||
<String>[
|
||||
'analyze_sample_code.dart',
|
||||
'--dart-ui-location=test/analyze-sample-code-test-dart-ui',
|
||||
'test/analyze-sample-code-test-input',
|
||||
],
|
||||
);
|
||||
final List<String> stdoutLines = process.stdout.toString().split('\n');
|
||||
final List<String> stderrLines = process.stderr.toString().split('\n');
|
||||
expect(process.exitCode, isNot(equals(0)));
|
||||
expect(stderrLines, containsAll(<String>[
|
||||
'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(<String>[
|
||||
// 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.',
|
||||
]));
|
||||
});
|
||||
}
|
71
dev/bots/test/analyze_snippet_code_test.dart
Normal file
71
dev/bots/test/analyze_snippet_code_test.dart
Normal file
@ -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',
|
||||
<String>['analyze_snippet_code.dart', '--no-include-dart-ui', 'test/analyze-snippet-code-test-input'],
|
||||
);
|
||||
final List<String> stdoutLines = process.stdout.toString().split('\n');
|
||||
final List<String> stderrLines = process.stderr.toString().split('\n');
|
||||
expect(process.exitCode, isNot(equals(0)));
|
||||
expect(stderrLines, containsAll(<Object>[
|
||||
'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(<String>[
|
||||
'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',
|
||||
<String>[
|
||||
'analyze_snippet_code.dart',
|
||||
'--dart-ui-location=test/analyze-snippet-code-test-dart-ui',
|
||||
'test/analyze-snippet-code-test-input',
|
||||
],
|
||||
);
|
||||
final List<String> stdoutLines = process.stdout.toString().split('\n');
|
||||
final List<String> stderrLines = process.stderr.toString().split('\n');
|
||||
expect(process.exitCode, isNot(equals(0)));
|
||||
expect(stderrLines, containsAll(<String>[
|
||||
'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(<String>[
|
||||
// There is one snippet code section in the test's dummy dart:ui code.
|
||||
'Found 14 snippet code blocks',
|
||||
'Starting analysis of code snippets.',
|
||||
]));
|
||||
});
|
||||
}
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -3213,7 +3213,6 @@ mixin Diagnosticable {
|
||||
/// value: isCurrent,
|
||||
/// ifTrue: 'active',
|
||||
/// ifFalse: 'inactive',
|
||||
/// showName: false,
|
||||
/// ));
|
||||
///
|
||||
/// properties.add(DiagnosticsProperty<bool>('keepAlive', keepAlive));
|
||||
|
@ -171,7 +171,7 @@ class Checkbox extends StatefulWidget {
|
||||
/// Checkbox(
|
||||
/// value: true,
|
||||
/// onChanged: (_){},
|
||||
/// fillColor: MaterialStateProperty.resolveWith<Color>((states) {
|
||||
/// fillColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
|
||||
/// if (states.contains(MaterialState.disabled)) {
|
||||
/// return Colors.orange.withOpacity(.32);
|
||||
/// }
|
||||
|
@ -200,7 +200,7 @@ class Radio<T> extends StatefulWidget {
|
||||
/// value: 1,
|
||||
/// groupValue: 1,
|
||||
/// onChanged: (_){},
|
||||
/// fillColor: MaterialStateProperty.resolveWith<Color>((states) {
|
||||
/// fillColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
|
||||
/// if (states.contains(MaterialState.disabled)) {
|
||||
/// return Colors.orange.withOpacity(.32);
|
||||
/// }
|
||||
|
@ -238,7 +238,7 @@ class Switch extends StatelessWidget {
|
||||
/// Switch(
|
||||
/// value: true,
|
||||
/// onChanged: (_) => true,
|
||||
/// thumbColor: MaterialStateProperty.resolveWith<Color>((states) {
|
||||
/// thumbColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> 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<Color>((states) {
|
||||
/// thumbColor: MaterialStateProperty.resolveWith<Color>((Set<MaterialState> states) {
|
||||
/// if (states.contains(MaterialState.disabled)) {
|
||||
/// return Colors.orange.withOpacity(.48);
|
||||
/// }
|
||||
|
@ -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)),
|
||||
|
@ -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),
|
||||
/// ),
|
||||
|
@ -49,7 +49,6 @@ import 'image_provider.dart';
|
||||
/// fit: BoxFit.cover,
|
||||
/// ),
|
||||
/// border: Border.all(
|
||||
/// color: Colors.black,
|
||||
/// width: 8,
|
||||
/// ),
|
||||
/// borderRadius: BorderRadius.circular(12),
|
||||
|
@ -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>[
|
||||
/// 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>[
|
||||
/// Color(0xFF4285F4), // blue
|
||||
/// Color(0xFF34A853), // green
|
||||
|
@ -118,7 +118,6 @@ class AnnotationResult<T> {
|
||||
/// offset,
|
||||
/// Offset.zero & size,
|
||||
/// super.paint,
|
||||
/// clipBehavior: Clip.hardEdge,
|
||||
/// oldLayer: _clipRectLayer.layer,
|
||||
/// );
|
||||
/// }
|
||||
|
@ -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
|
||||
|
@ -198,6 +198,9 @@ class MethodChannel {
|
||||
///
|
||||
/// ```dart
|
||||
/// class Music {
|
||||
/// // Class cannot be instantiated.
|
||||
/// const Music._();
|
||||
///
|
||||
/// static const MethodChannel _channel = MethodChannel('music');
|
||||
///
|
||||
/// static Future<bool> 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<dynamic>? songs = await _channel.invokeMethod<List<dynamic>>('getSongs');
|
||||
/// return songs?.map(Song.fromJson).toList() ?? <Song>[];
|
||||
/// return songs?.cast<Map<String, Object?>>().map<Song>(Song.fromJson).toList() ?? <Song>[];
|
||||
/// }
|
||||
///
|
||||
/// static Future<void> play(Song song, double volume) async {
|
||||
@ -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<String, Object?> json) {
|
||||
/// return Song(json['id']! as String, json['title']! as String, json['artist']! as String);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
|
@ -44,7 +44,6 @@ enum CrossFadeState {
|
||||
/// ```dart
|
||||
/// Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) {
|
||||
/// return Stack(
|
||||
/// fit: StackFit.loose,
|
||||
/// children: <Widget>[
|
||||
/// Positioned(
|
||||
/// key: bottomChildKey,
|
||||
|
@ -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(),
|
||||
/// ),
|
||||
/// ),
|
||||
|
@ -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<void> initializeMessages(String locale) => Future<void>.value();
|
||||
|
||||
// Used by loadAll() to record LocalizationsDelegate.load() futures we're
|
||||
|
@ -108,7 +108,6 @@ import 'viewport.dart';
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
class PageController extends ScrollController {
|
||||
|
@ -27,7 +27,7 @@ import 'scroll_controller.dart';
|
||||
import 'transitions.dart';
|
||||
|
||||
// Examples can assume:
|
||||
// dynamic routeObserver;
|
||||
// late RouteObserver<Route<void>> routeObserver;
|
||||
// late NavigatorState navigator;
|
||||
// late BuildContext context;
|
||||
// Future<bool> askTheUserIfTheyAreSure() async { return true; }
|
||||
@ -1058,7 +1058,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
||||
/// defined in the same way.
|
||||
///
|
||||
/// ```dart
|
||||
/// PageRouteBuilder(
|
||||
/// PageRouteBuilder<void>(
|
||||
/// pageBuilder: (BuildContext context,
|
||||
/// Animation<double> animation,
|
||||
/// Animation<double> secondaryAnimation,
|
||||
@ -1105,7 +1105,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
||||
/// route has been popped off.
|
||||
///
|
||||
/// ```dart
|
||||
/// PageRouteBuilder(
|
||||
/// PageRouteBuilder<void>(
|
||||
/// pageBuilder: (BuildContext context,
|
||||
/// Animation<double> animation,
|
||||
/// Animation<double> secondaryAnimation,
|
||||
|
Loading…
Reference in New Issue
Block a user