Skip to content

Commit 172e28f

Browse files
authored
Add tooling to programmatically update diagnostic-related files (#6368)
This mostly moves some tooling from the SDK and doesn't need to be too closely reviewed. It adds `./dash_site generate-diagnostics` to update the `src/_data/linter_rules.json` and `src/content/tools/diagnostic-messages.md` files from sources in the SDK. **This is temporary and much of it will be expanded or replaced** with logic in the static site generator to generate individual diagnostic message pages. I'm proposing we land this so we can incrementally get to that point and enable simplifying the SDK implementation.
1 parent 9eed870 commit 172e28f

10 files changed

+1062
-18
lines changed

src/_data/linter_rules.json

+14-14
Large diffs are not rendered by default.

src/content/tools/diagnostic-messages.md

+35-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ description: Details for diagnostics produced by the Dart analyzer.
44
body_class: highlight-diagnostics
55
skipFreshness: true
66
---
7+
78
{%- comment %}
89
WARNING: Do NOT EDIT this file directly. It is autogenerated by the script in
910
`pkg/analyzer/tool/diagnostics/generate.dart` in the sdk repository.
@@ -28069,8 +28070,8 @@ comment for `f` uses a block comment style:
2806928070

2807028071
```dart
2807128072
[!/**!]
28072-
[! * Example.!]
28073-
[! */!]
28073+
[!* Example.!]
28074+
[!*/!]
2807428075
void f() {}
2807528076
```
2807628077

@@ -28690,6 +28691,36 @@ class C {
2869028691
}
2869128692
```
2869228693

28694+
### unnecessary_ignore
28695+
28696+
_The diagnostic '{0}' isn't produced at this location so it doesn't need to be
28697+
ignored._
28698+
28699+
_The diagnostic '{0}' isn't produced in this file so it doesn't need to be
28700+
ignored._
28701+
28702+
#### Description
28703+
28704+
The analyzer produces this diagnostic when an ignore is specified to ignore a diagnostic that isn't produced.
28705+
28706+
#### Example
28707+
28708+
The following code produces this diagnostic because the `unused_local_variable`
28709+
diagnostic isn't reported at the ignored location:
28710+
28711+
```dart
28712+
// ignore: [!unused_local_variable!]
28713+
void f() {}
28714+
```
28715+
28716+
#### Common fixes
28717+
28718+
Remove the ignore comment:
28719+
28720+
```dart
28721+
void f() {}
28722+
```
28723+
2869328724
### unnecessary_lambdas
2869428725

2869528726
_Closure should be a tearoff._
@@ -29219,8 +29250,8 @@ _Unnecessary use of multiple underscores._
2921929250
#### Description
2922029251

2922129252
The analyzer produces this diagnostic when an unused variable is named
29222-
with multiple underscores (for example `__`).
29223-
A single `_` wildcard variable can be used instead.
29253+
with multiple underscores (for example `__`). A single `_` wildcard variable
29254+
can be used instead.
2922429255

2922529256
#### Example
2922629257

tool/dart_site/lib/dart_site.dart

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'src/commands/check_links.dart';
1212
import 'src/commands/check_site_variable.dart';
1313
import 'src/commands/format_dart.dart';
1414
import 'src/commands/freshness.dart';
15+
import 'src/commands/generate_diagnostic_docs.dart';
1516
import 'src/commands/generate_effective_dart_toc.dart';
1617
import 'src/commands/refresh_excerpts.dart';
1718
import 'src/commands/serve.dart';
@@ -34,6 +35,7 @@ final class DartSiteCommandRunner extends CommandRunner<int> {
3435
addCommand(RefreshExcerptsCommand());
3536
addCommand(FormatDartCommand());
3637
addCommand(FreshnessCommand());
38+
addCommand(GenerateDiagnosticDocs());
3739
addCommand(GenerateEffectiveDartToc());
3840
addCommand(AnalyzeDartCommand());
3941
addCommand(TestDartCommand());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) 2025, 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:args/command_runner.dart';
6+
7+
import '../diagnostics/diagnostics.dart' as diagnostics;
8+
import '../diagnostics/linter.dart' as linter;
9+
10+
final class GenerateDiagnosticDocs extends Command<int> {
11+
GenerateDiagnosticDocs();
12+
13+
@override
14+
String get description => 'Generate and update diagnostic docs.';
15+
16+
@override
17+
String get name => 'generate-diagnostics';
18+
19+
@override
20+
Future<int> run() => _update();
21+
}
22+
23+
Future<int> _update() async {
24+
print('Updating src/_data/linter_rules.json...');
25+
await _updateLintInfo();
26+
27+
print('Updating src/content/tool/diagnostic-messages.md...');
28+
await _updateDiagnosticDocs();
29+
30+
return 0;
31+
}
32+
33+
Future<void> _updateLintInfo() async {
34+
await linter.fetchAndUpdate();
35+
}
36+
37+
Future<void> _updateDiagnosticDocs() async {
38+
await diagnostics.generate();
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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 'dart:io';
6+
7+
import 'package:path/path.dart' as path;
8+
9+
import '../utils.dart';
10+
import 'error_code_documentation_info.dart';
11+
import 'error_code_info.dart';
12+
13+
/// Generate the file `diagnostics.md` based on the documentation associated
14+
/// with the declarations of the error codes.
15+
Future<void> generate() async {
16+
final sink = File(_outputPath).openWrite();
17+
final messages = await Messages.retrieve();
18+
final generator = DocumentationGenerator(messages);
19+
generator.writeDocumentation(sink);
20+
await sink.flush();
21+
await sink.close();
22+
}
23+
24+
/// Compute the path to the file into which documentation is being generated.
25+
String get _outputPath => path.join(
26+
repositoryRoot, 'src', 'content', 'tools', 'diagnostic-messages.md');
27+
28+
/// An information holder containing information about a diagnostic that was
29+
/// extracted from the instance creation expression.
30+
class DiagnosticInformation {
31+
/// The name of the diagnostic.
32+
final String name;
33+
34+
/// The messages associated with the diagnostic.
35+
final List<String> messages;
36+
37+
/// The previous names by which this diagnostic has been known.
38+
final List<String> previousNames = [];
39+
40+
/// The documentation text associated with the diagnostic.
41+
String? documentation;
42+
43+
/// Initialize a newly created information holder with the given [name] and
44+
/// [message].
45+
DiagnosticInformation(this.name, String message) : messages = [message];
46+
47+
/// Return `true` if this diagnostic has documentation.
48+
bool get hasDocumentation => documentation != null;
49+
50+
/// Add the [message] to the list of messages associated with the diagnostic.
51+
void addMessage(String message) {
52+
if (!messages.contains(message)) {
53+
messages.add(message);
54+
}
55+
}
56+
57+
void addPreviousName(String previousName) {
58+
if (!previousNames.contains(previousName)) {
59+
previousNames.add(previousName);
60+
}
61+
}
62+
63+
/// Return the full documentation for this diagnostic.
64+
void writeOn(StringSink sink) {
65+
messages.sort();
66+
sink.writeln('### ${name.toLowerCase()}');
67+
for (final previousName in previousNames) {
68+
sink.writeln();
69+
final previousInLowerCase = previousName.toLowerCase();
70+
sink.writeln('<a id="$previousInLowerCase" aria-hidden="true"></a>'
71+
'_(Previously known as `$previousInLowerCase`)_');
72+
}
73+
for (final message in messages) {
74+
sink.writeln();
75+
for (final line in _split('_${_escape(message)}_')) {
76+
sink.writeln(line);
77+
}
78+
}
79+
sink.writeln();
80+
sink.writeln(documentation!);
81+
}
82+
83+
/// Return a version of the [text] in which characters that have special
84+
/// meaning in markdown have been escaped.
85+
String _escape(String text) {
86+
return text.replaceAll('_', '\\_');
87+
}
88+
89+
/// Split the [message] into multiple lines, each of which is less than 80
90+
/// characters long.
91+
List<String> _split(String message) {
92+
// This uses a brute force approach because we don't expect to have messages
93+
// that need to be split more than once.
94+
final length = message.length;
95+
if (length <= 80) {
96+
return [message];
97+
}
98+
final endIndex = message.lastIndexOf(' ', 80);
99+
if (endIndex < 0) {
100+
return [message];
101+
}
102+
return [message.substring(0, endIndex), message.substring(endIndex + 1)];
103+
}
104+
}
105+
106+
/// A class used to generate diagnostic documentation.
107+
class DocumentationGenerator {
108+
/// A map from the name of a diagnostic to the information about that
109+
/// diagnostic.
110+
final Map<String, DiagnosticInformation> infoByName = {};
111+
112+
/// Initialize a newly created documentation generator.
113+
DocumentationGenerator(Messages messages) {
114+
for (final classEntry in messages.analyzerMessages.entries) {
115+
_extractAllDocs(classEntry.key, classEntry.value);
116+
}
117+
for (final classEntry in messages.linterMessages.entries) {
118+
_extractAllDocs(classEntry.key, classEntry.value);
119+
}
120+
121+
_extractAllDocs('ParserErrorCode',
122+
messages.cfeToAnalyzerErrorCodeTables.analyzerCodeToInfo);
123+
}
124+
125+
/// Writes the documentation to [sink].
126+
void writeDocumentation(StringSink sink) {
127+
_writeHeader(sink);
128+
_writeGlossary(sink);
129+
_writeDiagnostics(sink);
130+
}
131+
132+
/// Extract documentation from all of the files containing the definitions of
133+
/// diagnostics.
134+
void _extractAllDocs(String className, Map<String, ErrorCodeInfo> messages) {
135+
for (final errorEntry in messages.entries) {
136+
final errorName = errorEntry.key;
137+
final errorCodeInfo = errorEntry.value;
138+
if (errorCodeInfo is AliasErrorCodeInfo) {
139+
continue;
140+
}
141+
final name = errorCodeInfo.sharedName ?? errorName;
142+
var info = infoByName[name];
143+
final message = convertTemplate(
144+
errorCodeInfo.computePlaceholderToIndexMap(),
145+
errorCodeInfo.problemMessage);
146+
if (info == null) {
147+
info = DiagnosticInformation(name, message);
148+
infoByName[name] = info;
149+
} else {
150+
info.addMessage(message);
151+
}
152+
final previousName = errorCodeInfo.previousName;
153+
if (previousName != null) {
154+
info.addPreviousName(previousName);
155+
}
156+
final docs = _extractDoc('$className.$errorName', errorCodeInfo);
157+
if (docs.isNotEmpty) {
158+
if (info.documentation != null) {
159+
throw StateError(
160+
'Documentation defined multiple times for ${info.name}');
161+
}
162+
info.documentation = docs;
163+
}
164+
}
165+
}
166+
167+
/// Extract documentation from the given [errorCodeInfo].
168+
String _extractDoc(String errorCode, ErrorCodeInfo errorCodeInfo) {
169+
final parsedComment =
170+
parseErrorCodeDocumentation(errorCode, errorCodeInfo.documentation);
171+
if (parsedComment == null) {
172+
return '';
173+
}
174+
return [
175+
for (final documentationPart in parsedComment)
176+
documentationPart.formatForDocumentation()
177+
].join('\n');
178+
}
179+
180+
/// Write the documentation for all of the diagnostics.
181+
void _writeDiagnostics(StringSink sink) {
182+
sink.write('''
183+
184+
## Diagnostics
185+
186+
The analyzer produces the following diagnostics for code that
187+
doesn't conform to the language specification or
188+
that might work in unexpected ways.
189+
190+
[bottom type]: https://dart.dev/null-safety/understanding-null-safety#top-and-bottom
191+
[debugPrint]: https://api.flutter.dev/flutter/foundation/debugPrint.html
192+
[ffi]: https://dart.dev/interop/c-interop
193+
[IEEE 754]: https://en.wikipedia.org/wiki/IEEE_754
194+
[irrefutable pattern]: https://dart.dev/resources/glossary#irrefutable-pattern
195+
[kDebugMode]: https://api.flutter.dev/flutter/foundation/kDebugMode-constant.html
196+
[meta-doNotStore]: https://pub.dev/documentation/meta/latest/meta/doNotStore-constant.html
197+
[meta-doNotSubmit]: https://pub.dev/documentation/meta/latest/meta/doNotSubmit-constant.html
198+
[meta-factory]: https://pub.dev/documentation/meta/latest/meta/factory-constant.html
199+
[meta-immutable]: https://pub.dev/documentation/meta/latest/meta/immutable-constant.html
200+
[meta-internal]: https://pub.dev/documentation/meta/latest/meta/internal-constant.html
201+
[meta-literal]: https://pub.dev/documentation/meta/latest/meta/literal-constant.html
202+
[meta-mustBeConst]: https://pub.dev/documentation/meta/latest/meta/mustBeConst-constant.html
203+
[meta-mustCallSuper]: https://pub.dev/documentation/meta/latest/meta/mustCallSuper-constant.html
204+
[meta-optionalTypeArgs]: https://pub.dev/documentation/meta/latest/meta/optionalTypeArgs-constant.html
205+
[meta-sealed]: https://pub.dev/documentation/meta/latest/meta/sealed-constant.html
206+
[meta-useResult]: https://pub.dev/documentation/meta/latest/meta/useResult-constant.html
207+
[meta-UseResult]: https://pub.dev/documentation/meta/latest/meta/UseResult-class.html
208+
[meta-visibleForOverriding]: https://pub.dev/documentation/meta/latest/meta/visibleForOverriding-constant.html
209+
[meta-visibleForTesting]: https://pub.dev/documentation/meta/latest/meta/visibleForTesting-constant.html
210+
[package-logging]: https://pub.dev/packages/logging
211+
[refutable pattern]: https://dart.dev/resources/glossary#refutable-pattern
212+
''');
213+
final errorCodes = infoByName.keys.toList();
214+
errorCodes.sort();
215+
for (final errorCode in errorCodes) {
216+
final info = infoByName[errorCode]!;
217+
if (info.hasDocumentation) {
218+
sink.writeln();
219+
info.writeOn(sink);
220+
}
221+
}
222+
}
223+
224+
/// Link to the glossary.
225+
void _writeGlossary(StringSink sink) {
226+
sink.write(r'''
227+
228+
[constant context]: /resources/glossary#constant-context
229+
[definite assignment]: /resources/glossary#definite-assignment
230+
[mixin application]: /resources/glossary#mixin-application
231+
[override inference]: /resources/glossary#override-inference
232+
[part file]: /resources/glossary#part-file
233+
[potentially non-nullable]: /resources/glossary#potentially-non-nullable
234+
[public library]: /resources/glossary#public-library
235+
''');
236+
}
237+
238+
/// Write the header of the file.
239+
void _writeHeader(StringSink sink) {
240+
sink.write('''
241+
---
242+
title: Diagnostic messages
243+
description: Details for diagnostics produced by the Dart analyzer.
244+
body_class: highlight-diagnostics
245+
skipFreshness: true
246+
---
247+
248+
{%- comment %}
249+
WARNING: Do NOT EDIT this file directly. It is autogenerated by the script in
250+
`pkg/analyzer/tool/diagnostics/generate.dart` in the sdk repository.
251+
Update instructions: https://github.com/dart-lang/site-www/issues/1949
252+
{% endcomment -%}
253+
254+
This page lists diagnostic messages produced by the Dart analyzer,
255+
with details about what those messages mean and how you can fix your code.
256+
For more information about the analyzer, see
257+
[Customizing static analysis](/tools/analysis).
258+
''');
259+
}
260+
}

0 commit comments

Comments
 (0)