Skip to content

Commit d914839

Browse files
bwilkersoncommit-bot@chromium.org
authored andcommitted
Add tests for diagnostic docs
This tests that the docs have been generated and that the code samples in the docs generate the expected errors at the expected locations. Change-Id: Iff254537c15c58d6c5dde2bb2bb739e52f156b50 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/108320 Reviewed-by: Konstantin Shcheglov <[email protected]> Commit-Queue: Brian Wilkerson <[email protected]>
1 parent 33a5745 commit d914839

File tree

5 files changed

+285
-10
lines changed

5 files changed

+285
-10
lines changed

pkg/analyzer/lib/src/error/codes.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ class CompileTimeErrorCode extends ErrorCode {
166166
//
167167
// ```dart
168168
// union(Map<String, String> a, List<String> b, Map<String, String> c) =>
169-
// {...a, ...b, ...c};
169+
// !{...a, ...b, ...c}!;
170170
// ```
171171
//
172172
// The list `b` can only be spread into a set, and the maps `a` and `c` can

pkg/analyzer/test/test_all.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'instrumentation/test_all.dart' as instrumentation;
1313
import 'parse_compilation_unit_test.dart' as parse_compilation_unit;
1414
import 'source/test_all.dart' as source;
1515
import 'src/test_all.dart' as src;
16+
import 'verify_diagnostics_test.dart' as verify_diagnostics;
1617
import 'verify_docs_test.dart' as verify_docs;
1718
import 'verify_tests_test.dart' as verify_tests;
1819

@@ -27,6 +28,7 @@ main() {
2728
parse_compilation_unit.main();
2829
source.main();
2930
src.main();
31+
verify_diagnostics.main();
3032
verify_docs.main();
3133
verify_tests.main();
3234
}, name: 'analyzer');
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
6+
import 'package:analyzer/dart/analysis/results.dart';
7+
import 'package:analyzer/dart/analysis/session.dart';
8+
import 'package:analyzer/dart/ast/ast.dart';
9+
import 'package:analyzer/dart/ast/token.dart';
10+
import 'package:analyzer/error/error.dart';
11+
import 'package:analyzer/file_system/physical_file_system.dart';
12+
import 'package:front_end/src/testing/package_root.dart' as package_root;
13+
import 'package:path/src/context.dart';
14+
import 'package:test/test.dart';
15+
16+
import '../tool/diagnostics/generate.dart';
17+
import 'src/dart/resolution/driver_resolution.dart';
18+
19+
/// Validate the documentation associated with the declarations of the error
20+
/// codes.
21+
void main() async {
22+
Context pathContext = PhysicalResourceProvider.INSTANCE.pathContext;
23+
//
24+
// Validate that the input to the generator is correct.
25+
//
26+
String packageRoot = pathContext.normalize(package_root.packageRoot);
27+
String analyzerPath = pathContext.join(packageRoot, 'analyzer');
28+
List<String> docPaths = [
29+
pathContext.join(
30+
analyzerPath, 'lib', 'src', 'dart', 'error', 'hint_codes.dart'),
31+
pathContext.join(analyzerPath, 'lib', 'src', 'error', 'codes.dart'),
32+
];
33+
34+
DocumentationValidator validator = DocumentationValidator(docPaths);
35+
validator.validate();
36+
//
37+
// Validate that the generator has been run.
38+
//
39+
String outputPath =
40+
pathContext.join(analyzerPath, 'tool', 'diagnostics', 'diagnostics.md');
41+
String actualContent =
42+
PhysicalResourceProvider.INSTANCE.getFile(outputPath).readAsStringSync();
43+
44+
StringBuffer sink = StringBuffer();
45+
DocumentationGenerator generator = DocumentationGenerator(docPaths);
46+
generator.writeDocumentation(sink);
47+
String expectedContent = sink.toString();
48+
49+
if (actualContent != expectedContent) {
50+
fail('The diagnostic documentation needs to be regenerated.\n'
51+
'Please run tool/diagnostics/generate.dart.');
52+
}
53+
}
54+
55+
/// A class used to validate diagnostic documentation.
56+
class DocumentationValidator {
57+
/// The absolute paths of the files containing the declarations of the error
58+
/// codes.
59+
final List<String> docPaths;
60+
61+
/// The buffer to which validation errors are written.
62+
final StringBuffer buffer = StringBuffer();
63+
64+
/// The path to the file currently being verified.
65+
String filePath;
66+
67+
/// A flag indicating whether the [filePath] has already been written to the
68+
/// buffer.
69+
bool hasWrittenFilePath = false;
70+
71+
/// The name of the error code currently being verified.
72+
String codeName;
73+
74+
/// A flag indicating whether the [codeName] has already been written to the
75+
/// buffer.
76+
bool hasWrittenCodeName = false;
77+
78+
/// Initialize a newly created documentation validator.
79+
DocumentationValidator(this.docPaths);
80+
81+
/// Validate the documentation.
82+
void validate() async {
83+
AnalysisContextCollection collection = new AnalysisContextCollection(
84+
includedPaths: docPaths,
85+
resourceProvider: PhysicalResourceProvider.INSTANCE);
86+
for (String docPath in docPaths) {
87+
_validateFile(_parse(collection, docPath));
88+
}
89+
if (buffer.isNotEmpty) {
90+
fail(buffer.toString());
91+
}
92+
}
93+
94+
/// Extract documentation from the given [field] declaration.
95+
List<String> _extractDoc(FieldDeclaration field) {
96+
Token comments = field.firstTokenAfterCommentAndMetadata.precedingComments;
97+
if (comments == null) {
98+
return null;
99+
}
100+
List<String> docs = [];
101+
while (comments != null) {
102+
String lexeme = comments.lexeme;
103+
if (lexeme.startsWith('// TODO')) {
104+
break;
105+
} else if (lexeme.startsWith('// ')) {
106+
docs.add(lexeme.substring(3));
107+
} else if (lexeme == '//') {
108+
docs.add('');
109+
}
110+
comments = comments.next;
111+
}
112+
if (docs.isEmpty) {
113+
return null;
114+
}
115+
return docs;
116+
}
117+
118+
_SnippetData _extractSnippetData(String snippet) {
119+
int rangeStart = snippet.indexOf('!');
120+
if (rangeStart < 0) {
121+
_reportProblem('No error range in example');
122+
return _SnippetData(snippet, -1, 0);
123+
}
124+
int rangeEnd = snippet.indexOf('!', rangeStart + 1);
125+
return _SnippetData(
126+
snippet.substring(0, rangeStart) +
127+
snippet.substring(rangeStart + 1, rangeEnd) +
128+
snippet.substring(rangeEnd + 1),
129+
rangeStart,
130+
rangeEnd - rangeStart - 1);
131+
}
132+
133+
/// Extract the snippets of Dart code between the start (inclusive) and end
134+
/// (exclusive) indexes.
135+
List<String> _extractSnippets(List<String> lines, int start, int end) {
136+
List<String> snippets = [];
137+
int snippetStart = lines.indexOf('```dart', start);
138+
while (snippetStart >= 0 && snippetStart < end) {
139+
int snippetEnd = lines.indexOf('```', snippetStart + 1);
140+
snippets.add(lines.sublist(snippetStart + 1, snippetEnd).join('\n'));
141+
snippetStart = lines.indexOf('```dart', snippetEnd + 1);
142+
}
143+
return snippets;
144+
}
145+
146+
/// Use the analysis context [collection] to parse the file at the given
147+
/// [path] and return the result.
148+
ParsedUnitResult _parse(AnalysisContextCollection collection, String path) {
149+
AnalysisSession session = collection.contextFor(path).currentSession;
150+
if (session == null) {
151+
throw new StateError('No session for "$path"');
152+
}
153+
ParsedUnitResult result = session.getParsedUnit(path);
154+
if (result.state != ResultState.VALID) {
155+
throw new StateError('Unable to parse "$path"');
156+
}
157+
return result;
158+
}
159+
160+
/// Report a problem with the current error code.
161+
void _reportProblem(String problem, {List<AnalysisError> errors = const []}) {
162+
if (!hasWrittenFilePath) {
163+
buffer.writeln();
164+
buffer.writeln('In $filePath');
165+
hasWrittenFilePath = true;
166+
}
167+
if (!hasWrittenCodeName) {
168+
buffer.writeln(' $codeName');
169+
hasWrittenCodeName = true;
170+
}
171+
buffer.writeln(' $problem');
172+
for (AnalysisError error in errors) {
173+
buffer.write(error.errorCode);
174+
buffer.write(' (');
175+
buffer.write(error.offset);
176+
buffer.write(', ');
177+
buffer.write(error.length);
178+
buffer.write(') ');
179+
buffer.writeln(error.message);
180+
}
181+
}
182+
183+
/// Extract documentation from the file that was parsed to produce the given
184+
/// [result].
185+
void _validateFile(ParsedUnitResult result) {
186+
filePath = result.path;
187+
hasWrittenFilePath = false;
188+
CompilationUnit unit = result.unit;
189+
for (CompilationUnitMember declaration in unit.declarations) {
190+
if (declaration is ClassDeclaration) {
191+
String className = declaration.name.name;
192+
for (ClassMember member in declaration.members) {
193+
if (member is FieldDeclaration) {
194+
List<String> docs = _extractDoc(member);
195+
if (docs != null) {
196+
VariableDeclaration variable = member.fields.variables[0];
197+
String variableName = variable.name.name;
198+
codeName = '$className.$variableName';
199+
hasWrittenCodeName = false;
200+
201+
int exampleStart = docs.indexOf('#### Example');
202+
int fixesStart = docs.indexOf('#### Common fixes');
203+
204+
List<String> exampleSnippets =
205+
_extractSnippets(docs, exampleStart + 1, fixesStart);
206+
for (String snippet in exampleSnippets) {
207+
_SnippetData data = _extractSnippetData(snippet);
208+
_validateSnippet(data.snippet, data.offset, data.length);
209+
}
210+
211+
List<String> fixesSnippets =
212+
_extractSnippets(docs, fixesStart + 1, docs.length);
213+
for (String snippet in fixesSnippets) {
214+
_validateSnippet(snippet, -1, 0);
215+
}
216+
}
217+
}
218+
}
219+
}
220+
}
221+
}
222+
223+
/// Resolve the [snippet]. If the [offset] is less than zero, then verify that
224+
/// no diagnostics are reported. If the [offset] is greater than or equal to
225+
/// zero, verify that one error whose name matches the current code is
226+
/// reported at that offset with the given [length].
227+
void _validateSnippet(String snippet, int offset, int length) async {
228+
// TODO(brianwilkerson) Implement this.
229+
DriverResolutionTest test = DriverResolutionTest();
230+
test.setUp();
231+
test.addTestFile(snippet);
232+
await test.resolveTestFile();
233+
List<AnalysisError> errors = test.result.errors;
234+
int errorCount = errors.length;
235+
if (offset < 0) {
236+
if (errorCount > 0) {
237+
_reportProblem('Expected no errors but found $errorCount.',
238+
errors: errors);
239+
}
240+
} else {
241+
if (errorCount == 0) {
242+
_reportProblem('Expected one error but found none.');
243+
} else if (errorCount == 1) {
244+
AnalysisError error = errors[0];
245+
if (error.errorCode != codeName) {
246+
_reportProblem(
247+
'Expected an error with code $codeName, found ${error.errorCode}.');
248+
}
249+
if (error.offset != offset) {
250+
_reportProblem(
251+
'Expected an error at $offset, found ${error.offset}.');
252+
}
253+
if (error.length != length) {
254+
_reportProblem(
255+
'Expected an error of length $length, found ${error.length}.');
256+
}
257+
} else {
258+
_reportProblem('Expected one error but found $errorCount.',
259+
errors: errors);
260+
}
261+
}
262+
}
263+
}
264+
265+
/// A data holder used to return multiple values when extracting an error range
266+
/// from a snippet.
267+
class _SnippetData {
268+
final String snippet;
269+
final int offset;
270+
final int length;
271+
272+
_SnippetData(this.snippet, this.offset, this.length);
273+
}

pkg/analyzer/tool/diagnostics/diagnostics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ The following code produces this diagnostic:
4444

4545
```dart
4646
union(Map<String, String> a, List<String> b, Map<String, String> c) =>
47-
{...a, ...b, ...c};
47+
!{...a, ...b, ...c}!;
4848
```
4949

5050
The list `b` can only be spread into a set, and the maps `a` and `c` can

pkg/analyzer/tool/diagnostics/generate.dart

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ void main() async {
2727
String outputPath =
2828
pathContext.join(analyzerPath, 'tool', 'diagnostics', 'diagnostics.md');
2929

30+
IOSink sink = File(outputPath).openWrite();
3031
DocumentationGenerator generator = DocumentationGenerator(docPaths);
31-
generator.writeDocumentation(outputPath);
32+
generator.writeDocumentation(sink);
33+
await sink.flush();
34+
await sink.close();
3235
}
3336

3437
/// A class used to generate diagnostic documentation.
@@ -47,13 +50,10 @@ class DocumentationGenerator {
4750
}
4851

4952
/// Write the documentation to the file at the given [outputPath].
50-
void writeDocumentation(String outputPath) async {
51-
IOSink sink = File(outputPath).openWrite();
53+
void writeDocumentation(StringSink sink) {
5254
_writeHeader(sink);
5355
// _writeGlossary(sink);
5456
_writeDiagnostics(sink);
55-
await sink.flush();
56-
await sink.close();
5757
}
5858

5959
/// Return a version of the [text] in which characters that have special
@@ -163,7 +163,7 @@ class DocumentationGenerator {
163163
}
164164

165165
/// Write the documentation for all of the diagnostics.
166-
void _writeDiagnostics(IOSink sink) {
166+
void _writeDiagnostics(StringSink sink) {
167167
sink.write('''
168168
169169
## Diagnostics
@@ -184,7 +184,7 @@ that might work in unexpected ways.
184184
}
185185

186186
// /// Write the glossary.
187-
// void _writeGlossary(IOSink sink) {
187+
// void _writeGlossary(StringSink sink) {
188188
// sink.write('''
189189
//
190190
//## Glossary
@@ -200,7 +200,7 @@ that might work in unexpected ways.
200200
// }
201201

202202
/// Write the header of the file.
203-
void _writeHeader(IOSink sink) {
203+
void _writeHeader(StringSink sink) {
204204
sink.write('''
205205
---
206206
title: Diagnostics

0 commit comments

Comments
 (0)