Skip to content

Commit 8b23e34

Browse files
stereotype441Commit Queue
authored andcommitted
[analyzer] Move some error reporting code into _fe_analyzer_shared.
The following classes are moved from `package:analyzer` to `package:_fe_analyzer_shared`: - `Diagnostic` - `DiagnosticMessage` - `DiagnosticMessageImpl` The following declarations are also moved, since they are needed by the above classes: - `formatList` - `Severity` - `Source` - `TimestampedData` There is no change to the analyzer public API, and `export` declarations have been added to the analyzer libraries that the declarations have been moved from, so that code depending on these declarations is unaffected. These changes are part of a larger arc of work that introduces methods `.withArguments` and `.at`, forming a literate API for reporting analyzer errors that looks roughly like this: diagnosticReporter.reportError( ERROR_CODE.withArguments(...arguments...).at(...location...)); Moving this code into `_fe_analyzer_shared` is necessary because scanner error codes are defined inside `_fe_analyzer_shared` (to allow the scanner to be shared between the analyzer and CFE). Hence, to avoid a circular depedency between `_fe_analyzer_shared` and `analyzer`, the `.withArguments` and `.at` methods will need to live in `_fe_analyzer_shared` too, as well as the classes representing the diagnostic messages they create. Note that there are some minor changes to `pkg/analysis_server_plugin/api.txt` and `pkg/analyzer_plugin/api.txt`; these have to do with the way the `api.txt` generator chooses to report referenced elements, and don't reflect actual API changes. Change-Id: I6a6a6964a5c46f4a0205ce0d85620669ce55eb3c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/444620 Reviewed-by: Brian Wilkerson <[email protected]> Commit-Queue: Paul Berry <[email protected]> Reviewed-by: Konstantin Shcheglov <[email protected]>
1 parent 105711a commit 8b23e34

File tree

15 files changed

+439
-389
lines changed

15 files changed

+439
-389
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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:_fe_analyzer_shared/src/base/analyzer_public_api.dart';
6+
import 'package:_fe_analyzer_shared/src/base/errors.dart';
7+
8+
/// A single message associated with a [Diagnostic], consisting of the text of
9+
/// the message and the location associated with it.
10+
///
11+
/// Clients may not extend, implement or mix-in this class.
12+
@AnalyzerPublicApi(
13+
message: 'Exported by package:analyzer/diagnostic/diagnostic.dart',
14+
)
15+
abstract class DiagnosticMessage {
16+
/// The absolute and normalized path of the file associated with this message.
17+
String get filePath;
18+
19+
/// The length of the source range associated with this message.
20+
int get length;
21+
22+
/// The zero-based offset from the start of the file to the beginning of the
23+
/// source range associated with this message.
24+
int get offset;
25+
26+
/// The URL containing documentation about this diagnostic message, if any.
27+
///
28+
/// Note: this should not be confused with the location in the user's code
29+
/// where the error was reported; that information can be obtained from
30+
/// [filePath], [length], and [offset].
31+
String? get url;
32+
33+
/// Gets the text of the message.
34+
///
35+
/// If [includeUrl] is `true`, and this diagnostic message has an associated
36+
/// URL, it is included in the returned value in a human-readable way.
37+
/// Clients that wish to present URLs as simple text can do this. If
38+
/// [includeUrl] is `false`, no URL is included in the returned value.
39+
/// Clients that have a special mechanism for presenting URLs (e.g. as a
40+
/// clickable link) should do this and then consult the [url] getter to access
41+
/// the URL.
42+
String messageText({required bool includeUrl});
43+
}
44+
45+
/// A concrete implementation of a diagnostic message.
46+
class DiagnosticMessageImpl implements DiagnosticMessage {
47+
@override
48+
final String filePath;
49+
50+
@override
51+
final int length;
52+
53+
final String _message;
54+
55+
@override
56+
final int offset;
57+
58+
@override
59+
final String? url;
60+
61+
/// Initialize a newly created message to represent a [message] reported in
62+
/// the file at the given [filePath] at the given [offset] and with the given
63+
/// [length].
64+
DiagnosticMessageImpl({
65+
required this.filePath,
66+
required this.length,
67+
required String message,
68+
required this.offset,
69+
required this.url,
70+
}) : _message = message;
71+
72+
@override
73+
String messageText({required bool includeUrl}) {
74+
if (includeUrl && url != null) {
75+
StringBuffer result = new StringBuffer(_message);
76+
if (!_message.endsWith('.')) {
77+
result.write('.');
78+
}
79+
result.write(' See $url');
80+
return result.toString();
81+
}
82+
return _message;
83+
}
84+
}
85+
86+
/// An indication of the severity of a [Diagnostic].
87+
@AnalyzerPublicApi(
88+
message: 'exported by package:analyzer/diagnostic/diagnostic.dart',
89+
)
90+
enum Severity { error, warning, info }

pkg/_fe_analyzer_shared/lib/src/base/errors.dart

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,32 @@
55
import 'dart:math';
66

77
import 'package:_fe_analyzer_shared/src/base/analyzer_public_api.dart';
8+
import 'package:_fe_analyzer_shared/src/base/diagnostic_message.dart';
9+
import 'package:_fe_analyzer_shared/src/base/source.dart';
810

911
import 'customized_codes.dart';
1012

13+
/// Inserts the given [arguments] into [pattern].
14+
///
15+
/// format('Hello, {0}!', ['John']) = 'Hello, John!'
16+
/// format('{0} are you {1}ing?', ['How', 'do']) = 'How are you doing?'
17+
/// format('{0} are you {1}ing?', ['What', 'read']) =
18+
/// 'What are you reading?'
19+
String formatList(String pattern, List<Object?>? arguments) {
20+
if (arguments == null || arguments.isEmpty) {
21+
assert(
22+
!pattern.contains(new RegExp(r'\{(\d+)\}')),
23+
'Message requires arguments, but none were provided.',
24+
);
25+
return pattern;
26+
}
27+
return pattern.replaceAllMapped(new RegExp(r'\{(\d+)\}'), (match) {
28+
String indexStr = match.group(1)!;
29+
int index = int.parse(indexStr);
30+
return arguments[index].toString();
31+
});
32+
}
33+
1134
/// A diagnostic code associated with an `AnalysisError`.
1235
///
1336
/// Generally, messages should follow the [Guide for Writing
@@ -25,6 +48,202 @@ typedef ErrorSeverity = DiagnosticSeverity;
2548
@Deprecated("Use 'DiagnosticType' instead.")
2649
typedef ErrorType = DiagnosticType;
2750

51+
/// A diagnostic, as defined by the [Diagnostic Design Guidelines][guidelines]:
52+
///
53+
/// > An indication of a specific problem at a specific location within the
54+
/// > source code being processed by a development tool.
55+
///
56+
/// Clients may not extend, implement or mix-in this class.
57+
///
58+
/// [guidelines]: https://github.com/dart-lang/sdk/blob/main/pkg/analyzer/doc/implementation/diagnostics.md
59+
@AnalyzerPublicApi(
60+
message: 'Exported by package:analyzer/diagnostic/diagnostic.dart',
61+
)
62+
class Diagnostic {
63+
/// The diagnostic code associated with the diagnostic.
64+
final DiagnosticCode diagnosticCode;
65+
66+
/// A list of messages that provide context for understanding the problem
67+
/// being reported. The list will be empty if there are no such messages.
68+
final List<DiagnosticMessage> contextMessages;
69+
70+
/// Data associated with this diagnostic, specific for [diagnosticCode].
71+
@Deprecated('Use an expando instead')
72+
final Object? data;
73+
74+
/// A description of how to fix the problem, or `null` if there is no such
75+
/// description.
76+
final String? correctionMessage;
77+
78+
/// A message describing what is wrong and why.
79+
final DiagnosticMessage problemMessage;
80+
81+
/// The source in which the diagnostic occurred, or `null` if unknown.
82+
final Source source;
83+
84+
Diagnostic.forValues({
85+
required this.source,
86+
required int offset,
87+
required int length,
88+
DiagnosticCode? diagnosticCode,
89+
@Deprecated("Pass a value for 'diagnosticCode' instead")
90+
DiagnosticCode? errorCode,
91+
required String message,
92+
this.correctionMessage,
93+
this.contextMessages = const [],
94+
@Deprecated('Use an expando instead') this.data,
95+
}) : diagnosticCode = _useNonNullCodeBetween(diagnosticCode, errorCode),
96+
problemMessage = new DiagnosticMessageImpl(
97+
filePath: source.fullName,
98+
length: length,
99+
message: message,
100+
offset: offset,
101+
url: null,
102+
);
103+
104+
/// Initialize a newly created diagnostic.
105+
///
106+
/// The diagnostic is associated with the given [source] and is located at the
107+
/// given [offset] with the given [length]. The diagnostic will have the given
108+
/// [errorCode] and the list of [arguments] will be used to complete the
109+
/// message and correction. If any [contextMessages] are provided, they will
110+
/// be recorded with the diagnostic.
111+
factory Diagnostic.tmp({
112+
required Source source,
113+
required int offset,
114+
required int length,
115+
DiagnosticCode? diagnosticCode,
116+
@Deprecated("Pass a value for 'diagnosticCode' instead")
117+
DiagnosticCode? errorCode,
118+
List<Object?> arguments = const [],
119+
List<DiagnosticMessage> contextMessages = const [],
120+
@Deprecated('Use an expando instead') Object? data,
121+
}) {
122+
DiagnosticCode code = _useNonNullCodeBetween(diagnosticCode, errorCode);
123+
assert(
124+
arguments.length == code.numParameters,
125+
'Message $code requires ${code.numParameters} '
126+
'argument${code.numParameters == 1 ? '' : 's'}, but '
127+
'${arguments.length} '
128+
'argument${arguments.length == 1 ? ' was' : 's were'} '
129+
'provided',
130+
);
131+
String message = formatList(code.problemMessage, arguments);
132+
String? correctionTemplate = code.correctionMessage;
133+
String? correctionMessage;
134+
if (correctionTemplate != null) {
135+
correctionMessage = formatList(correctionTemplate, arguments);
136+
}
137+
138+
return new Diagnostic.forValues(
139+
source: source,
140+
offset: offset,
141+
length: length,
142+
diagnosticCode: code,
143+
message: message,
144+
correctionMessage: correctionMessage,
145+
contextMessages: contextMessages,
146+
// ignore: deprecated_member_use_from_same_package
147+
data: data,
148+
);
149+
}
150+
151+
/// The template used to create the correction to be displayed for this
152+
/// diagnostic, or `null` if there is no correction information for this
153+
/// error. The correction should indicate how the user can fix the error.
154+
@Deprecated("Use 'correctionMessage' instead.")
155+
String? get correction => correctionMessage;
156+
157+
@Deprecated("Use 'diagnosticCode' instead")
158+
DiagnosticCode get errorCode => diagnosticCode;
159+
160+
@override
161+
int get hashCode {
162+
int hashCode = offset;
163+
hashCode ^= message.hashCode;
164+
hashCode ^= source.hashCode;
165+
return hashCode;
166+
}
167+
168+
/// The number of characters from the offset to the end of the source which
169+
/// encompasses the compilation error.
170+
int get length => problemMessage.length;
171+
172+
/// The message to be displayed for this diagnostic.
173+
///
174+
/// The message indicates what is wrong and why it is wrong.
175+
String get message => problemMessage.messageText(includeUrl: true);
176+
177+
/// The character offset from the beginning of the source (zero based) where
178+
/// the diagnostic occurred.
179+
int get offset => problemMessage.offset;
180+
181+
Severity get severity {
182+
switch (diagnosticCode.severity) {
183+
case DiagnosticSeverity.ERROR:
184+
return Severity.error;
185+
case DiagnosticSeverity.WARNING:
186+
return Severity.warning;
187+
case DiagnosticSeverity.INFO:
188+
return Severity.info;
189+
default:
190+
throw new StateError('Invalid severity: ${diagnosticCode.severity}');
191+
}
192+
}
193+
194+
@override
195+
bool operator ==(Object other) {
196+
if (identical(other, this)) {
197+
return true;
198+
}
199+
// prepare the other Diagnostic.
200+
if (other is Diagnostic) {
201+
// Quick checks.
202+
if (!identical(diagnosticCode, other.diagnosticCode)) {
203+
return false;
204+
}
205+
if (offset != other.offset || length != other.length) {
206+
return false;
207+
}
208+
// Deep checks.
209+
if (message != other.message) {
210+
return false;
211+
}
212+
if (source != other.source) {
213+
return false;
214+
}
215+
return true;
216+
}
217+
return false;
218+
}
219+
220+
@override
221+
String toString() {
222+
StringBuffer buffer = new StringBuffer();
223+
buffer.write(source.fullName);
224+
buffer.write("(");
225+
buffer.write(offset);
226+
buffer.write("..");
227+
buffer.write(offset + length - 1);
228+
buffer.write("): ");
229+
buffer.write(message);
230+
return buffer.toString();
231+
}
232+
233+
/// The non-`null` [DiagnosticCode] value between the two parameters.
234+
static DiagnosticCode _useNonNullCodeBetween(
235+
DiagnosticCode? diagnosticCode,
236+
DiagnosticCode? errorCode,
237+
) {
238+
if ((diagnosticCode == null) == (errorCode == null)) {
239+
throw new ArgumentError(
240+
"Exactly one of 'diagnosticCode' and 'errorCode' may be passed",
241+
);
242+
}
243+
return diagnosticCode ?? errorCode!;
244+
}
245+
}
246+
28247
/// An error code associated with an `AnalysisError`.
29248
///
30249
/// Generally, messages should follow the [Guide for Writing

0 commit comments

Comments
 (0)