Unverified Commit 169bb1b7 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Fix the sample code analyzer to properly handle `missing_identifier` errors (#87593)

This fixes how the sample analyzer handles missing_identifier errors. It was looking at the wrong line, and missing an else clause, so it was silently allowing missing_identifier errors to pass.

In addition, this fixes the sample generation so that it uses the correct filename for the output files: it previously was looking for the first line that had a filename, which was meant to indicate a non-generated line. This change adds a new Line.generated constructor for generated lines, so that they can also have the correct filename associated with them.
parent cc63c814
...@@ -109,7 +109,7 @@ Future<void> run(List<String> arguments) async { ...@@ -109,7 +109,7 @@ Future<void> run(List<String> arguments) async {
// Analyze all the sample code in the repo // Analyze all the sample code in the repo
print('$clock Sample code...'); print('$clock Sample code...');
await runCommand(dart, await runCommand(dart,
<String>[path.join(flutterRoot, 'dev', 'bots', 'analyze_sample_code.dart'), '--verbose'], <String>[path.join(flutterRoot, 'dev', 'bots', 'analyze_sample_code.dart')],
workingDirectory: flutterRoot, workingDirectory: flutterRoot,
); );
......
...@@ -263,7 +263,6 @@ class SampleChecker { ...@@ -263,7 +263,6 @@ class SampleChecker {
/// Computes the headers needed for each sample file. /// Computes the headers needed for each sample file.
List<Line> get headers { List<Line> get headers {
return _headers ??= <String>[ return _headers ??= <String>[
'// generated code',
'// ignore_for_file: directives_ordering', '// ignore_for_file: directives_ordering',
'// ignore_for_file: unnecessary_import', '// ignore_for_file: unnecessary_import',
'// ignore_for_file: unused_import', '// ignore_for_file: unused_import',
...@@ -275,12 +274,9 @@ class SampleChecker { ...@@ -275,12 +274,9 @@ class SampleChecker {
"import 'dart:typed_data';", "import 'dart:typed_data';",
"import 'dart:ui' as ui;", "import 'dart:ui' as ui;",
"import 'package:flutter_test/flutter_test.dart';", "import 'package:flutter_test/flutter_test.dart';",
for (final File file in _listDartFiles(Directory(_defaultFlutterPackage))) ...<String>[ for (final File file in _listDartFiles(Directory(_defaultFlutterPackage)))
'',
'// ${file.path}',
"import 'package:flutter/${path.basename(file.path)}';", "import 'package:flutter/${path.basename(file.path)}';",
], ].map<Line>((String code) => Line.generated(code: code, filename: 'headers')).toList();
].map<Line>((String code) => Line(code)).toList();
} }
List<Line>? _headers; List<Line>? _headers;
...@@ -495,7 +491,6 @@ class SampleChecker { ...@@ -495,7 +491,6 @@ class SampleChecker {
} else if (_codeBlockStartRegex.hasMatch(trimmedLine)) { } else if (_codeBlockStartRegex.hasMatch(trimmedLine)) {
assert(block.isEmpty); assert(block.isEmpty);
startLine = Line( startLine = Line(
'',
filename: relativeFilePath, filename: relativeFilePath,
line: lineNumber + 1, line: lineNumber + 1,
indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length, indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length,
...@@ -507,7 +502,7 @@ class SampleChecker { ...@@ -507,7 +502,7 @@ class SampleChecker {
final RegExpMatch? 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.generated(filename: relativeFilePath, line: lineNumber + 1, indent: 3);
inPreamble = true; inPreamble = true;
} else if (sampleMatch != null) { } else if (sampleMatch != null) {
inSnippet = sampleMatch != null && (sampleMatch[1] == 'sample' || sampleMatch[1] == 'dartpad'); inSnippet = sampleMatch != null && (sampleMatch[1] == 'sample' || sampleMatch[1] == 'dartpad');
...@@ -519,7 +514,6 @@ class SampleChecker { ...@@ -519,7 +514,6 @@ class SampleChecker {
dartpadCount++; dartpadCount++;
} }
startLine = Line( startLine = Line(
'',
filename: relativeFilePath, filename: relativeFilePath,
line: lineNumber + 1, line: lineNumber + 1,
indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length, indent: line.indexOf(_dartDocPrefixWithSpace) + _dartDocPrefixWithSpace.length,
...@@ -543,7 +537,9 @@ class SampleChecker { ...@@ -543,7 +537,9 @@ class SampleChecker {
} }
} }
if (!silent) if (!silent)
print('Found ${sections.length} snippet code blocks, $sampleCount sample code sections, and $dartpadCount dartpad sections.'); print('Found ${sections.length} snippet code blocks, '
'$sampleCount sample code sections, and '
'$dartpadCount dartpad sections.');
for (final Section section in sections) { for (final Section section in sections) {
final String path = _writeSection(section).path; final String path = _writeSection(section).path;
if (sectionMap != null) if (sectionMap != null)
...@@ -634,10 +630,10 @@ linter: ...@@ -634,10 +630,10 @@ linter:
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(''), Line.generated(code: section.dartVersionOverride ?? '', filename: section.start.filename),
...headers, ...headers,
const Line(''), Line.generated(filename: section.start.filename),
Line('// From: ${section.start.filename}:${section.start.line}'), Line.generated(code: '// From: ${section.start.filename}:${section.start.line}', filename: section.start.filename),
...section.code, ...section.code,
]; ];
outputFile.writeAsStringSync(mainContents.map<String>((Line line) => line.code).join('\n')); outputFile.writeAsStringSync(mainContents.map<String>((Line line) => line.code).join('\n'));
...@@ -661,10 +657,8 @@ linter: ...@@ -661,10 +657,8 @@ linter:
return line.startsWith('Building flutter tool...'); return line.startsWith('Building flutter tool...');
}); });
// Check out the stderr to see if the analyzer had it's own issues. // Check out the stderr to see if the analyzer had it's own issues.
if (stderr.isNotEmpty && (stderr.first.contains(' issues found. (ran in ') || stderr.first.contains(' issue found. (ran in '))) { if (stderr.isNotEmpty && stderr.first.contains(RegExp(r' issues? found\. \(ran in '))) {
// The "23 issues found" message goes onto stderr, which is concatenated first.
stderr.removeAt(0); stderr.removeAt(0);
// If there's an "issues found" message, we put a blank line on stdout before it.
if (stderr.isNotEmpty && stderr.last.isEmpty) { if (stderr.isNotEmpty && stderr.last.isEmpty) {
stderr.removeLast(); stderr.removeLast();
} }
...@@ -741,7 +735,6 @@ linter: ...@@ -741,7 +735,6 @@ linter:
message, message,
errorCode, errorCode,
Line( Line(
'',
filename: file.path, filename: file.path,
line: lineNumber, line: lineNumber,
), ),
...@@ -777,7 +770,7 @@ linter: ...@@ -777,7 +770,7 @@ linter:
columnNumber, columnNumber,
message, message,
errorCode, errorCode,
Line('', filename: file.path, line: lineNumber), Line(filename: file.path, line: lineNumber),
), ),
); );
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);
...@@ -793,48 +786,43 @@ linter: ...@@ -793,48 +786,43 @@ linter:
} }
final Line actualLine = actualSection.code[lineNumber - 1]; final Line actualLine = actualSection.code[lineNumber - 1];
if (actualLine.filename == null) { 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) { if (errorCode == 'missing_identifier' && lineNumber > 1) {
if (fileContents[lineNumber - 2].endsWith(',')) { // For a missing identifier on a generated line, it is very often because of a
final Line actualLine = sections[file.path]!.code[lineNumber - 2]; // trailing comma on the previous line, and so we want to provide a better message
addAnalysisError( // and the previous line as the error location, since that appears in the original
file, // source, and can be more easily located.
AnalysisError( final Line previousCodeLine = sections[file.path]!.code[lineNumber - 2];
type, if (previousCodeLine.code.contains(RegExp(r',\s*$'))) {
actualLine.line, line = previousCodeLine.line;
actualLine.indent + fileContents[lineNumber - 2].length - 1, column = previousCodeLine.indent + previousCodeLine.code.length - 1;
'Unexpected comma at end of sample code.', errorMessage = 'Unexpected comma at end of sample code.';
errorCode, source = previousCodeLine;
actualLine,
),
);
} }
} else {
addAnalysisError(
file,
AnalysisError(
type,
lineNumber - 1,
columnNumber,
message,
errorCode,
actualLine,
),
);
} }
} else { } else {
addAnalysisError( line = actualLine.line;
file, column = actualLine.indent + columnNumber;
AnalysisError(
type,
actualLine.line,
actualLine.indent + columnNumber,
message,
errorCode,
actualLine,
),
);
} }
addAnalysisError(
file,
AnalysisError(
type,
line,
column,
errorMessage,
errorCode,
source,
),
);
} }
} }
if (_exitCode == 1 && analysisErrors.isEmpty && !unknownAnalyzerErrors) { if (_exitCode == 1 && analysisErrors.isEmpty && !unknownAnalyzerErrors) {
...@@ -877,7 +865,7 @@ linter: ...@@ -877,7 +865,7 @@ linter:
// treated as a separate code block. // treated as a separate code block.
if (block[index] == '' || block[index] == '// ...') { if (block[index] == '' || block[index] == '// ...') {
if (subline == null) if (subline == null)
throw SampleCheckerException('${Line('', filename: line.filename, line: line.line + index, indent: line.indent)}: ' throw SampleCheckerException('${Line(filename: line.filename, line: line.line + index, indent: line.indent)}: '
'Unexpected blank line or "// ..." line near start of subblock in sample code.'); 'Unexpected blank line or "// ..." line near start of subblock in sample code.');
subblocks += 1; subblocks += 1;
subsections.add(_processBlock(subline, buffer)); subsections.add(_processBlock(subline, buffer));
...@@ -889,7 +877,7 @@ linter: ...@@ -889,7 +877,7 @@ linter:
buffer.add('/${block[index]}'); // so that it doesn't start with "// " and get caught in this again buffer.add('/${block[index]}'); // so that it doesn't start with "// " and get caught in this again
} else { } else {
subline ??= Line( subline ??= Line(
block[index], code: block[index],
filename: line.filename, filename: line.filename,
line: line.line + index, line: line.line + index,
indent: line.indent, indent: line.indent,
...@@ -912,11 +900,17 @@ linter: ...@@ -912,11 +900,17 @@ linter:
/// 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 = 'unknown', this.line = -1, this.indent = 0}); 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 String filename;
final int line; final int line;
final int indent; final int indent;
final String code; final String code;
final bool generated;
String toStringWithColumn(int column) { String toStringWithColumn(int column) {
if (column != null && indent != null) { if (column != null && indent != null) {
...@@ -943,7 +937,7 @@ class Section { ...@@ -943,7 +937,7 @@ class Section {
for (int i = 0; i < code.length; ++i) { for (int i = 0; i < code.length; ++i) {
codeLines.add( codeLines.add(
Line( Line(
code[i], code: code[i],
filename: firstLine.filename, filename: firstLine.filename,
line: firstLine.line + i, line: firstLine.line + i,
indent: firstLine.indent, indent: firstLine.indent,
...@@ -959,7 +953,7 @@ class Section { ...@@ -959,7 +953,7 @@ class Section {
for (int i = 0; i < code.length; ++i) { for (int i = 0; i < code.length; ++i) {
codeLines.add( codeLines.add(
Line( Line(
code[i], code: code[i],
filename: firstLine.filename, filename: firstLine.filename,
line: firstLine.line + i, line: firstLine.line + i,
indent: firstLine.indent, indent: firstLine.indent,
...@@ -967,12 +961,12 @@ class Section { ...@@ -967,12 +961,12 @@ class Section {
); );
} }
return Section(<Line>[ return Section(<Line>[
Line(prefix), Line.generated(code: prefix, filename: firstLine.filename, line: 0),
...codeLines, ...codeLines,
Line(postfix), Line.generated(code: postfix, filename: firstLine.filename, line: 0),
]); ]);
} }
Line get start => code.firstWhere((Line line) => line.filename != null); Line get start => code.firstWhere((Line line) => !line.generated);
final List<Line> code; final List<Line> code;
final String? dartVersionOverride; final String? dartVersionOverride;
......
...@@ -113,6 +113,14 @@ ...@@ -113,6 +113,14 @@
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// ///
/// {@tool snippet}
/// snippet with trailing comma
///
/// ```dart
/// const SizedBox(),
/// ```
/// {@end-tool}
///
/// {@tool dartpad --template=stateless_widget_material} /// {@tool dartpad --template=stateless_widget_material}
/// Dartpad with null-safe syntax /// Dartpad with null-safe syntax
/// ///
......
...@@ -24,7 +24,7 @@ void main() { ...@@ -24,7 +24,7 @@ void main() {
..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 dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:117: child: Text(title),', 'In sample starting at dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:125: 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)",
'dev/bots/test/analyze-sample-code-test-input/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)',
...@@ -36,12 +36,14 @@ void main() { ...@@ -36,12 +36,14 @@ void main() {
'>>> info: Prefer const over final for declarations (prefer_const_declarations)', '>>> info: Prefer const over final for declarations (prefer_const_declarations)',
'dev/bots/test/analyze-sample-code-test-input/known_broken_documentation.dart:112:25: final int foo = null;', 'dev/bots/test/analyze-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)",
'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.', 'Found 2 sample code errors.',
'' ''
]); ]);
expect(stdoutLines, <String>[ expect(stdoutLines, <String>[
'Found 8 snippet code blocks, 0 sample code sections, and 2 dartpad sections.', 'Found 9 snippet code blocks, 0 sample code sections, and 2 dartpad sections.',
'Starting analysis of code samples.', 'Starting analysis of code samples.',
'', '',
]); ]);
...@@ -60,7 +62,7 @@ void main() { ...@@ -60,7 +62,7 @@ void main() {
expect(process.exitCode, isNot(equals(0))); expect(process.exitCode, isNot(equals(0)));
expect(stdoutLines, equals(<String>[ expect(stdoutLines, equals(<String>[
// There is one sample code section in the test's dummy dart:ui code. // 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.', 'Found 9 snippet code blocks, 1 sample code sections, and 2 dartpad sections.',
'', '',
])); ]));
}); });
......
...@@ -142,7 +142,7 @@ import 'text_style.dart'; ...@@ -142,7 +142,7 @@ import 'text_style.dart';
/// fontSize: 30, /// fontSize: 30,
/// height: 1.5, /// height: 1.5,
/// ), /// ),
/// ), /// )
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// ///
...@@ -183,7 +183,7 @@ import 'text_style.dart'; ...@@ -183,7 +183,7 @@ import 'text_style.dart';
/// fontFamily: 'Roboto', /// fontFamily: 'Roboto',
/// height: 1.5, /// height: 1.5,
/// ), /// ),
/// ), /// )
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// ///
...@@ -226,7 +226,7 @@ import 'text_style.dart'; ...@@ -226,7 +226,7 @@ import 'text_style.dart';
/// height: 1, /// height: 1,
/// forceStrutHeight: true, /// forceStrutHeight: true,
/// ), /// ),
/// ), /// )
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
/// ///
......
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