Unverified Commit b33c7891 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Fix the sample analyzer to analyze dart:ui and make the analyzer null safe (#80742)

parent a84ac2ec
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
// To run this, from the root of the Flutter repository: // To run this, from the root of the Flutter repository:
// bin/cache/dart-sdk/bin/dart dev/bots/analyze_sample_code.dart // bin/cache/dart-sdk/bin/dart dev/bots/analyze_sample_code.dart
// @dart= 2.12
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
...@@ -16,6 +18,7 @@ import 'package:watcher/watcher.dart'; ...@@ -16,6 +18,7 @@ import 'package:watcher/watcher.dart';
final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script)))); final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script))));
final String _defaultFlutterPackage = path.join(_flutterRoot, 'packages', 'flutter', 'lib'); final String _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'); final String _flutter = path.join(_flutterRoot, 'bin', Platform.isWindows ? 'flutter.bat' : 'flutter');
void main(List<String> arguments) { void main(List<String> arguments) {
...@@ -33,6 +36,20 @@ void main(List<String> arguments) { ...@@ -33,6 +36,20 @@ void main(List<String> arguments) {
negatable: false, negatable: false,
help: 'Print verbose output for the analysis process.', 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,
negatable: true,
help: 'Includes the dart:ui code supplied by the engine in the analysis.',
);
argParser.addFlag( argParser.addFlag(
'help', 'help',
defaultsTo: false, defaultsTo: false,
...@@ -61,7 +78,20 @@ void main(List<String> arguments) { ...@@ -61,7 +78,20 @@ void main(List<String> arguments) {
flutterPackage = Directory(_defaultFlutterPackage); flutterPackage = Directory(_defaultFlutterPackage);
} }
Directory tempDirectory; 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')) { if (parsedArguments.wasParsed('temp')) {
final String tempArg = parsedArguments['temp'] as String; final String tempArg = parsedArguments['temp'] as String;
tempDirectory = Directory(path.join(Directory.systemTemp.absolute.path, path.basename(tempArg))); tempDirectory = Directory(path.join(Directory.systemTemp.absolute.path, path.basename(tempArg)));
...@@ -78,13 +108,19 @@ void main(List<String> arguments) { ...@@ -78,13 +108,19 @@ void main(List<String> arguments) {
} }
if (parsedArguments['interactive'] != null) { if (parsedArguments['interactive'] != null) {
_runInteractive(tempDirectory, flutterPackage, parsedArguments['interactive'] as String); _runInteractive(
tempDir: tempDirectory,
flutterPackage: flutterPackage,
filePath: parsedArguments['interactive'] as String,
dartUiLocation: includeDartUi ? dartUiLocation : null,
);
} else { } else {
try { try {
exitCode = SampleChecker( exitCode = SampleChecker(
flutterPackage, flutterPackage,
tempDirectory: tempDirectory, tempDirectory: tempDirectory,
verbose: parsedArguments['verbose'] as bool, verbose: parsedArguments['verbose'] as bool,
dartUiLocation: includeDartUi ? dartUiLocation : null,
).checkSamples(); ).checkSamples();
} on SampleCheckerException catch (e) { } on SampleCheckerException catch (e) {
stderr.write(e); stderr.write(e);
...@@ -96,8 +132,8 @@ void main(List<String> arguments) { ...@@ -96,8 +132,8 @@ void main(List<String> arguments) {
class SampleCheckerException implements Exception { class SampleCheckerException implements Exception {
SampleCheckerException(this.message, {this.file, this.line}); SampleCheckerException(this.message, {this.file, this.line});
final String message; final String message;
final String file; final String? file;
final int line; final int? line;
@override @override
String toString() { String toString() {
...@@ -126,11 +162,30 @@ class SampleCheckerException implements Exception { ...@@ -126,11 +162,30 @@ class SampleCheckerException implements Exception {
/// don't necessarily match. It does, however, print the source of the /// don't necessarily match. It does, however, print the source of the
/// problematic line. /// problematic line.
class SampleChecker { class SampleChecker {
SampleChecker(this._flutterPackage, {Directory tempDirectory, this.verbose = false}) /// Creates a [SampleChecker].
: _tempDirectory = tempDirectory, ///
_keepTmp = tempDirectory != null { /// The positional argument is the path to the the package directory for the
_tempDirectory ??= Directory.systemTemp.createTempSync('flutter_analyze_sample_code.'); /// 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").
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 /// The prefix of each comment line
static const String _dartDocPrefix = '///'; static const String _dartDocPrefix = '///';
...@@ -166,11 +221,18 @@ class SampleChecker { ...@@ -166,11 +221,18 @@ class SampleChecker {
/// The temporary directory where all output is written. This will be deleted /// The temporary directory where all output is written. This will be deleted
/// automatically if there are no errors. /// automatically if there are no errors.
Directory _tempDirectory; final Directory _tempDirectory;
/// The package directory for the flutter package within the flutter root dir. /// The package directory for the flutter package within the flutter root dir.
final Directory _flutterPackage; 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 /// A serial number so that we can create unique expression names when we
/// generate them. /// generate them.
int _expressionId = 0; int _expressionId = 0;
...@@ -180,7 +242,7 @@ class SampleChecker { ...@@ -180,7 +242,7 @@ class SampleChecker {
// Once the snippets tool has been precompiled by Dart, this contains the AOT // Once the snippets tool has been precompiled by Dart, this contains the AOT
// snapshot. // snapshot.
String _snippetsSnapshotPath; String? _snippetsSnapshotPath;
/// Finds the location of the snippets script. /// Finds the location of the snippets script.
String get _snippetsExecutable { String get _snippetsExecutable {
...@@ -219,7 +281,7 @@ class SampleChecker { ...@@ -219,7 +281,7 @@ class SampleChecker {
].map<Line>((String code) => Line(code)).toList(); ].map<Line>((String code) => Line(code)).toList();
} }
List<Line> _headers; List<Line>? _headers;
/// Checks all the samples in the Dart files in [_flutterPackage] for errors. /// Checks all the samples in the Dart files in [_flutterPackage] for errors.
int checkSamples() { int checkSamples() {
...@@ -228,12 +290,19 @@ class SampleChecker { ...@@ -228,12 +290,19 @@ class SampleChecker {
try { try {
final Map<String, Section> sections = <String, Section>{}; final Map<String, Section> sections = <String, Section>{};
final Map<String, Sample> snippets = <String, Sample>{}; final Map<String, Sample> snippets = <String, Sample>{};
_extractSamples(_listDartFiles(_flutterPackage, recursive: true), sectionMap: sections, sampleMap: snippets); if (_dartUiLocation != null && !_dartUiLocation!.existsSync()) {
stderr.writeln('Unable to analyze engine dart samples at ${_dartUiLocation!.path}.');
}
final List<File> filesToAnalyze = <File>[
..._listDartFiles(_flutterPackage, recursive: true),
if (_dartUiLocation != null && _dartUiLocation!.existsSync()) ... _listDartFiles(_dartUiLocation!, recursive: true),
];
_extractSamples(filesToAnalyze, sectionMap: sections, sampleMap: snippets);
errors = _analyze(_tempDirectory, sections, snippets); errors = _analyze(_tempDirectory, sections, snippets);
} finally { } finally {
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
for (final String filePath in errors.keys) { for (final String filePath in errors.keys) {
errors[filePath].forEach(stderr.writeln); errors[filePath]!.forEach(stderr.writeln);
} }
stderr.writeln('\nFound ${errors.length} sample code errors.'); stderr.writeln('\nFound ${errors.length} sample code errors.');
} }
...@@ -248,7 +317,7 @@ class SampleChecker { ...@@ -248,7 +317,7 @@ class SampleChecker {
} }
// If we made a snapshot, remove it (so as not to clutter up the tree). // If we made a snapshot, remove it (so as not to clutter up the tree).
if (_snippetsSnapshotPath != null) { if (_snippetsSnapshotPath != null) {
final File snapshot = File(_snippetsSnapshotPath); final File snapshot = File(_snippetsSnapshotPath!);
if (snapshot.existsSync()) { if (snapshot.existsSync()) {
snapshot.deleteSync(); snapshot.deleteSync();
} }
...@@ -285,7 +354,7 @@ class SampleChecker { ...@@ -285,7 +354,7 @@ class SampleChecker {
} else { } else {
return Process.runSync( return Process.runSync(
_dartExecutable, _dartExecutable,
<String>[path.canonicalize(_snippetsSnapshotPath), ...args], <String>[path.canonicalize(_snippetsSnapshotPath!), ...args],
workingDirectory: workingDirectory, workingDirectory: workingDirectory,
); );
} }
...@@ -307,7 +376,7 @@ class SampleChecker { ...@@ -307,7 +376,7 @@ class SampleChecker {
...sample.args, ...sample.args,
]; ];
if (verbose) if (verbose)
print('Generating sample for ${sample.start?.filename}:${sample.start?.line}'); print('Generating sample for ${sample.start.filename}:${sample.start.line}');
final ProcessResult process = _runSnippetsScript(args); final ProcessResult process = _runSnippetsScript(args);
if (verbose) if (verbose)
stderr.write('${process.stderr}'); stderr.write('${process.stderr}');
...@@ -324,14 +393,19 @@ class SampleChecker { ...@@ -324,14 +393,19 @@ class SampleChecker {
/// Extracts the samples from the Dart files in [files], writes them /// Extracts the samples from the Dart files in [files], writes them
/// to disk, and adds them to the appropriate [sectionMap] or [sampleMap]. /// to disk, and adds them to the appropriate [sectionMap] or [sampleMap].
void _extractSamples(List<File> files, {Map<String, Section> sectionMap, Map<String, Sample> sampleMap, bool silent = false}) { void _extractSamples(
List<File> files, {
required Map<String, Section> sectionMap,
required Map<String, Sample> sampleMap,
bool silent = false,
}) {
final List<Section> sections = <Section>[]; final List<Section> sections = <Section>[];
final List<Sample> samples = <Sample>[]; final List<Sample> samples = <Sample>[];
int dartpadCount = 0; int dartpadCount = 0;
int sampleCount = 0; int sampleCount = 0;
for (final File file in files) { for (final File file in files) {
final String relativeFilePath = path.relative(file.path, from: _flutterPackage.path); final String relativeFilePath = path.relative(file.path, from: _flutterRoot);
final List<String> sampleLines = file.readAsLinesSync(); final List<String> sampleLines = file.readAsLinesSync();
final List<Section> preambleSections = <Section>[]; final List<Section> preambleSections = <Section>[];
// Whether or not we're in the file-wide preamble section ("Examples can assume"). // Whether or not we're in the file-wide preamble section ("Examples can assume").
...@@ -342,11 +416,11 @@ class SampleChecker { ...@@ -342,11 +416,11 @@ class SampleChecker {
bool inSnippet = false; bool inSnippet = false;
// Whether or not we're in a '```dart' segment. // Whether or not we're in a '```dart' segment.
bool inDart = false; bool inDart = false;
String dartVersionOverride; String? dartVersionOverride;
int lineNumber = 0; int lineNumber = 0;
final List<String> block = <String>[]; final List<String> block = <String>[];
List<String> snippetArgs = <String>[]; List<String> snippetArgs = <String>[];
Line startLine; late Line startLine;
for (final String line in sampleLines) { for (final String line in sampleLines) {
lineNumber += 1; lineNumber += 1;
final String trimmedLine = line.trim(); final String trimmedLine = line.trim();
...@@ -425,7 +499,7 @@ class SampleChecker { ...@@ -425,7 +499,7 @@ class SampleChecker {
} }
} }
if (!inSampleSection) { if (!inSampleSection) {
final Match sampleMatch = _dartDocSampleBeginRegex.firstMatch(trimmedLine); final RegExpMatch? sampleMatch = _dartDocSampleBeginRegex.firstMatch(trimmedLine);
if (line == '// Examples can assume:') { if (line == '// Examples can assume:') {
assert(block.isEmpty); assert(block.isEmpty);
startLine = Line('', filename: relativeFilePath, line: lineNumber + 1, indent: 3); startLine = Line('', filename: relativeFilePath, line: lineNumber + 1, indent: 3);
...@@ -447,7 +521,7 @@ class SampleChecker { ...@@ -447,7 +521,7 @@ class SampleChecker {
); );
if (sampleMatch[2] != null) { if (sampleMatch[2] != null) {
// There are arguments to the snippet tool to keep track of. // There are arguments to the snippet tool to keep track of.
snippetArgs = _splitUpQuotedArgs(sampleMatch[2]).toList(); snippetArgs = _splitUpQuotedArgs(sampleMatch[2]!).toList();
} else { } else {
snippetArgs = <String>[]; snippetArgs = <String>[];
} }
...@@ -507,7 +581,7 @@ class SampleChecker { ...@@ -507,7 +581,7 @@ class SampleChecker {
// by an equals sign), add a "--" in front so that they parse as options. // by an equals sign), add a "--" in front so that they parse as options.
return matches.map<String>((Match match) { return matches.map<String>((Match match) {
String option = ''; String option = '';
if (match[1] != null && !match[1].startsWith('-')) { if (match[1] != null && !match[1]!.startsWith('-')) {
option = '--'; option = '--';
} }
if (match[2] != null) { if (match[2] != null) {
...@@ -543,7 +617,7 @@ dependencies: ...@@ -543,7 +617,7 @@ dependencies:
final String sectionId = _createNameFromSource('snippet', section.start.filename, section.start.line); final String sectionId = _createNameFromSource('snippet', section.start.filename, section.start.line);
final File outputFile = File(path.join(_tempDirectory.path, '$sectionId.dart'))..createSync(recursive: true); final File outputFile = File(path.join(_tempDirectory.path, '$sectionId.dart'))..createSync(recursive: true);
final List<Line> mainContents = <Line>[ final List<Line> mainContents = <Line>[
if (section.dartVersionOverride != null) Line(section.dartVersionOverride) else const Line(''), if (section.dartVersionOverride != null) Line(section.dartVersionOverride!) else const Line(''),
...headers, ...headers,
const Line(''), const Line(''),
Line('// From: ${section.start.filename}:${section.start.line}'), Line('// From: ${section.start.filename}:${section.start.line}'),
...@@ -554,7 +628,7 @@ dependencies: ...@@ -554,7 +628,7 @@ dependencies:
} }
/// Invokes the analyzer on the given [directory] and returns the stdout. /// Invokes the analyzer on the given [directory] and returns the stdout.
List<String> _runAnalyzer(Directory directory, {bool silent}) { List<String> _runAnalyzer(Directory directory, {bool silent = true}) {
if (!silent) if (!silent)
print('Starting analysis of code samples.'); print('Starting analysis of code samples.');
_createConfigurationFiles(directory); _createConfigurationFiles(directory);
...@@ -603,7 +677,7 @@ dependencies: ...@@ -603,7 +677,7 @@ dependencies:
final Map<String, List<AnalysisError>> analysisErrors = <String, List<AnalysisError>>{}; final Map<String, List<AnalysisError>> analysisErrors = <String, List<AnalysisError>>{};
void addAnalysisError(File file, AnalysisError error) { void addAnalysisError(File file, AnalysisError error) {
if (analysisErrors.containsKey(file.path)) { if (analysisErrors.containsKey(file.path)) {
analysisErrors[file.path].add(error); analysisErrors[file.path]!.add(error);
} else { } else {
analysisErrors[file.path] = <AnalysisError>[error]; analysisErrors[file.path] = <AnalysisError>[error];
} }
...@@ -621,21 +695,21 @@ dependencies: ...@@ -621,21 +695,21 @@ dependencies:
bool unknownAnalyzerErrors = false; bool unknownAnalyzerErrors = false;
final int headerLength = headers.length + 3; final int headerLength = headers.length + 3;
for (final String error in errors) { for (final String error in errors) {
final RegExpMatch match = errorPattern.firstMatch(error); final RegExpMatch? match = errorPattern.firstMatch(error);
if (match == null) { if (match == null) {
stderr.writeln('Analyzer output: $error'); stderr.writeln('Analyzer output: $error');
unknownAnalyzerErrors = true; unknownAnalyzerErrors = true;
continue; continue;
} }
final String type = match.namedGroup('type'); final String type = match.namedGroup('type')!;
final String message = match.namedGroup('description'); final String message = match.namedGroup('description')!;
final File file = File(path.join(_tempDirectory.path, match.namedGroup('file'))); final File file = File(path.join(_tempDirectory.path, match.namedGroup('file')));
final List<String> fileContents = file.readAsLinesSync(); final List<String> fileContents = file.readAsLinesSync();
final bool isSnippet = path.basename(file.path).startsWith('snippet.'); final bool isSnippet = path.basename(file.path).startsWith('snippet.');
final bool isSample = path.basename(file.path).startsWith('sample.'); final bool isSample = path.basename(file.path).startsWith('sample.');
final String line = match.namedGroup('line'); final String line = match.namedGroup('line')!;
final String column = match.namedGroup('column'); final String column = match.namedGroup('column')!;
final String errorCode = match.namedGroup('code'); final String errorCode = match.namedGroup('code')!;
final int lineNumber = int.parse(line, radix: 10) - (isSnippet ? headerLength : 0); final int lineNumber = int.parse(line, radix: 10) - (isSnippet ? headerLength : 0);
final int columnNumber = int.parse(column, radix: 10); final int columnNumber = int.parse(column, radix: 10);
...@@ -692,7 +766,7 @@ dependencies: ...@@ -692,7 +766,7 @@ dependencies:
throw SampleCheckerException('Failed to parse error message: $error', file: file.path, line: lineNumber); throw SampleCheckerException('Failed to parse error message: $error', file: file.path, line: lineNumber);
} }
final Section actualSection = sections[file.path]; final Section actualSection = sections[file.path]!;
if (actualSection == null) { if (actualSection == null) {
throw SampleCheckerException( throw SampleCheckerException(
"Unknown section for ${file.path}. Maybe the temporary directory wasn't empty?", "Unknown section for ${file.path}. Maybe the temporary directory wasn't empty?",
...@@ -702,10 +776,10 @@ dependencies: ...@@ -702,10 +776,10 @@ dependencies:
} }
final Line actualLine = actualSection.code[lineNumber - 1]; final Line actualLine = actualSection.code[lineNumber - 1];
if (actualLine?.filename == null) { if (actualLine.filename == null) {
if (errorCode == 'missing_identifier' && lineNumber > 1) { if (errorCode == 'missing_identifier' && lineNumber > 1) {
if (fileContents[lineNumber - 2].endsWith(',')) { if (fileContents[lineNumber - 2].endsWith(',')) {
final Line actualLine = sections[file.path].code[lineNumber - 2]; final Line actualLine = sections[file.path]!.code[lineNumber - 2];
addAnalysisError( addAnalysisError(
file, file,
AnalysisError( AnalysisError(
...@@ -779,7 +853,7 @@ dependencies: ...@@ -779,7 +853,7 @@ dependencies:
} else { } else {
final List<String> buffer = <String>[]; final List<String> buffer = <String>[];
int subblocks = 0; int subblocks = 0;
Line subline; Line? subline;
final List<Section> subsections = <Section>[]; final List<Section> subsections = <Section>[];
for (int index = 0; index < block.length; index += 1) { for (int index = 0; index < block.length; index += 1) {
// Each section of the dart code that is either split by a blank line, or with '// ...' is // Each section of the dart code that is either split by a blank line, or with '// ...' is
...@@ -821,7 +895,7 @@ dependencies: ...@@ -821,7 +895,7 @@ dependencies:
/// A class to represent a line of input code. /// A class to represent a line of input code.
class Line { class Line {
const Line(this.code, {this.filename, this.line, this.indent}); const Line(this.code, {this.filename = 'unknown', this.line = -1, this.indent = 0});
final String filename; final String filename;
final int line; final int line;
final int indent; final int indent;
...@@ -883,10 +957,10 @@ class Section { ...@@ -883,10 +957,10 @@ class Section {
} }
Line get start => code.firstWhere((Line line) => line.filename != null); Line get start => code.firstWhere((Line line) => line.filename != null);
final List<Line> code; final List<Line> code;
final String dartVersionOverride; final String? dartVersionOverride;
Section copyWith({String dartVersionOverride}) { Section copyWith({String? dartVersionOverride}) {
return Section(code, dartVersionOverride: dartVersionOverride); return Section(code, dartVersionOverride: dartVersionOverride ?? this.dartVersionOverride);
} }
} }
...@@ -895,10 +969,14 @@ class Section { ...@@ -895,10 +969,14 @@ class Section {
/// regular snippets, because they must be injected into templates in order to be /// regular snippets, because they must be injected into templates in order to be
/// analyzed. /// analyzed.
class Sample { class Sample {
Sample({this.start, List<String> input, List<String> args, this.serial}) { Sample({
this.input = input.toList(); required this.start,
this.args = args.toList(); required List<String> input,
} required List<String> args,
required this.serial,
}) : input = input.toList(),
args = args.toList(),
contents = <String>[];
final Line start; final Line start;
final int serial; final int serial;
List<String> input; List<String> input;
...@@ -936,16 +1014,16 @@ class AnalysisError { ...@@ -936,16 +1014,16 @@ class AnalysisError {
final int column; final int column;
final String message; final String message;
final String errorCode; final String errorCode;
final Line source; final Line? source;
final Sample sample; final Sample? sample;
@override @override
String toString() { String toString() {
if (source != null) { if (source != null) {
return '${source.toStringWithColumn(column)}\n>>> $type: $message ($errorCode)'; return '${source!.toStringWithColumn(column)}\n>>> $type: $message ($errorCode)';
} else if (sample != null) { } else if (sample != null) {
return 'In sample starting at ' return 'In sample starting at '
'${sample.start.filename}:${sample.start.line}:${sample.contents[line - 1]}\n' '${sample!.start.filename}:${sample!.start.line}:${sample!.contents[line - 1]}\n'
'>>> $type: $message ($errorCode)'; '>>> $type: $message ($errorCode)';
} else { } else {
return '<source unknown>:$line:$column\n>>> $type: $message ($errorCode)'; return '<source unknown>:$line:$column\n>>> $type: $message ($errorCode)';
...@@ -953,21 +1031,28 @@ class AnalysisError { ...@@ -953,21 +1031,28 @@ class AnalysisError {
} }
} }
Future<void> _runInteractive(Directory tempDir, Directory flutterPackage, String filePath) async { 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); filePath = path.isAbsolute(filePath) ? filePath : path.join(path.current, filePath);
final File file = File(filePath); final File file = File(filePath);
if (!file.existsSync()) { if (!file.existsSync()) {
throw 'Path ${file.absolute.path} does not exist.'; throw 'Path ${file.absolute.path} does not exist ($filePath).';
} }
if (!path.isWithin(_flutterRoot, file.absolute.path)) { if (!path.isWithin(_flutterRoot, file.absolute.path) &&
throw 'Path ${file.absolute.path} is not within the flutter root: $_flutterRoot'; (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) { if (tempDir == null) {
tempDir = Directory.systemTemp.createTempSync('flutter_analyze_sample_code.'); tempDir = Directory.systemTemp.createTempSync('flutter_analyze_sample_code.');
ProcessSignal.sigint.watch().listen((_) { ProcessSignal.sigint.watch().listen((_) {
print('Deleting temp files...'); print('Deleting temp files...');
tempDir.deleteSync(recursive: true); tempDir!.deleteSync(recursive: true);
exit(0); exit(0);
}); });
print('Using temp dir ${tempDir.path}'); print('Using temp dir ${tempDir.path}');
...@@ -982,7 +1067,7 @@ Future<void> _runInteractive(Directory tempDir, Directory flutterPackage, String ...@@ -982,7 +1067,7 @@ Future<void> _runInteractive(Directory tempDir, Directory flutterPackage, String
stderr.writeln('\u001B[2J\u001B[H'); // Clears the old results from the terminal. stderr.writeln('\u001B[2J\u001B[H'); // Clears the old results from the terminal.
if (errors.isNotEmpty) { if (errors.isNotEmpty) {
for (final String filePath in errors.keys) { for (final String filePath in errors.keys) {
errors[filePath].forEach(stderr.writeln); errors[filePath]!.forEach(stderr.writeln);
} }
stderr.writeln('\nFound ${errors.length} errors.'); stderr.writeln('\nFound ${errors.length} errors.');
} else { } else {
...@@ -1012,10 +1097,9 @@ Future<void> _runInteractive(Directory tempDir, Directory flutterPackage, String ...@@ -1012,10 +1097,9 @@ Future<void> _runInteractive(Directory tempDir, Directory flutterPackage, String
case 'q': case 'q':
print('Exiting...'); print('Exiting...');
exit(0); exit(0);
break;
case 'r': case 'r':
print('Deleting temp files...'); print('Deleting temp files...');
tempDir.deleteSync(recursive: true); tempDir!.deleteSync(recursive: true);
rerun(); rerun();
break; break;
} }
......
// 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.
// @dart = 2.12
// This is a dummy dart:ui package for the sample code analyzer tests to use.
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}
/// A sample if using keepToString to prevent replacement by a supercall.
///
/// ```dart
/// class MyStringBuffer {
/// StringBuffer _buffer = StringBuffer();
///
/// @keepToString
/// @override
/// String toString() {
/// return _buffer.toString();
/// }
/// }
/// ```
/// {@end-tool}
const _KeepToString keepToString = _KeepToString();
class _KeepToString {
const _KeepToString();
}
\ No newline at end of file
...@@ -7,35 +7,42 @@ import 'dart:io'; ...@@ -7,35 +7,42 @@ import 'dart:io';
import 'common.dart'; import 'common.dart';
void main() { void main() {
test('analyze_sample_code', () { // 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( final ProcessResult process = Process.runSync(
'../../bin/cache/dart-sdk/bin/dart', '../../bin/cache/dart-sdk/bin/dart',
<String>['analyze_sample_code.dart', 'test/analyze-sample-code-test-input'], <String>['analyze_sample_code.dart', '--no-include-dart-ui', 'test/analyze-sample-code-test-input'],
); );
final List<String> stdoutLines = process.stdout.toString().split('\n'); final List<String> stdoutLines = process.stdout.toString().split('\n');
final List<String> stderrLines = process.stderr.toString().split('\n') final List<String> stderrLines = process.stderr.toString().split('\n')
..removeWhere((String line) => line.startsWith('Analyzer output:') || line.startsWith('Building flutter tool...')); ..removeWhere((String line) => line.startsWith('Analyzer output:') || line.startsWith('Building flutter tool...'));
expect(process.exitCode, isNot(equals(0))); expect(process.exitCode, isNot(equals(0)));
expect(stderrLines, <String>[ expect(stderrLines, <String>[
'In sample starting at known_broken_documentation.dart:117:bool? _visible = true;', 'In sample starting at dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:117:bool? _visible = true;',
'>>> info: Use late for private members with non-nullable type (use_late_for_private_fields_and_variables)', '>>> info: Use late for private members with non-nullable type (use_late_for_private_fields_and_variables)',
'In sample starting at known_broken_documentation.dart:117: child: Text(title),', 'In sample starting at dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:117: child: Text(title),',
'>>> error: The final variable \'title\' can\'t be read because it is potentially unassigned at this point (read_potentially_unassigned_final)', '>>> error: The final variable \'title\' can\'t be read because it is potentially unassigned at this point (read_potentially_unassigned_final)',
'known_broken_documentation.dart:30:9: new Opacity(', 'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:30:9: new Opacity(',
'>>> info: Unnecessary new keyword (unnecessary_new)', '>>> info: Unnecessary new keyword (unnecessary_new)',
'known_broken_documentation.dart:62:9: new Opacity(', 'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:62:9: new Opacity(',
'>>> info: Unnecessary new keyword (unnecessary_new)', '>>> info: Unnecessary new keyword (unnecessary_new)',
'known_broken_documentation.dart:95:9: const text0 = Text(\'Poor wandering ones!\');', 'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:95:9: const text0 = Text(\'Poor wandering ones!\');',
'>>> info: Specify type annotations (always_specify_types)', '>>> info: Specify type annotations (always_specify_types)',
'known_broken_documentation.dart:103:9: const text1 = _Text(\'Poor wandering ones!\');', 'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:103:9: const text1 = _Text(\'Poor wandering ones!\');',
'>>> info: Specify type annotations (always_specify_types)', '>>> info: Specify type annotations (always_specify_types)',
'known_broken_documentation.dart:111:9: final String? bar = \'Hello\';', '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)', '>>> info: Prefer const over final for declarations (prefer_const_declarations)',
'known_broken_documentation.dart:111:23: final String? bar = \'Hello\';', 'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:111:23: final String? bar = \'Hello\';',
'>>> info: Use a non-nullable type for a final variable initialized with a non-nullable value (unnecessary_nullable_for_final_variable_declarations)', '>>> info: Use a non-nullable type for a final variable initialized with a non-nullable value (unnecessary_nullable_for_final_variable_declarations)',
'known_broken_documentation.dart:112:9: final int foo = null;', '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)', '>>> info: Prefer const over final for declarations (prefer_const_declarations)',
'known_broken_documentation.dart:112:25: final int foo = null;', '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)', '>>> error: A value of type \'Null\' can\'t be assigned to a variable of type \'int\' (invalid_assignment)',
'', '',
'Found 2 sample code errors.', 'Found 2 sample code errors.',
...@@ -46,5 +53,23 @@ void main() { ...@@ -46,5 +53,23 @@ void main() {
'Starting analysis of code samples.', 'Starting analysis of code samples.',
'', '',
]); ]);
}, skip: Platform.isWindows); });
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');
expect(process.exitCode, isNot(equals(0)));
expect(stdoutLines, equals(<String>[
// There is one sample code section in the test's dummy dart:ui code.
'Found 8 snippet code blocks, 1 sample code sections, and 2 dartpad sections.',
'',
]));
});
} }
...@@ -179,7 +179,9 @@ class SnippetGenerator { ...@@ -179,7 +179,9 @@ class SnippetGenerator {
description.add(line); description.add(line);
} else { } else {
assert(language != null); assert(language != null);
components.last.contents.add(line); if (components.isNotEmpty) {
components.last.contents.add(line);
}
} }
} }
return <_ComponentTuple>[ return <_ComponentTuple>[
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment