Skip to content

Commit d7fa12f

Browse files
stereotype441Commit Queue
authored andcommitted
[analyzer] Lock down the behavior of ResultComparator with unit tests.
In a follow-up CL, I intend to rewrite `ResultComparator` in a way that doesn't depend on its base class, `AstComparator` (so that `AstComparator` can be deleted). This change prepares for the rewrite by adding unit tests for `ResultComparator`. I've discovered some bugs in `ResultComparator` in the process: - The failure messages reported when one of the two nodes was `null` were reversed. I've gone ahead and fixed this as part of this CL, since the fix was easy and didn't affect any tests beyond the tests that I've added. - The failure message reported when a node had the wrong type contained an unnecessary newline. I've gone ahead and fixed this as part of this CL, since the fix was easy and didn't affect any tests beyond the tests that I've added. - When comparing identifiers, if the actual identifier is a synthetic identifier, and the expected identifier is any identifier at all, the identifiers are considered to match. This is clearly not the intended behavior; the intended behavior is that the expected identifier must be `_s_` to match a synthetic identifier. I've documented this bug with a TODO, which I'll address in a follow-up CL. - When comparing comments, only the references inside the comment are compared; the text of the comment is ignored. This seems inconsistent with the other behaviors of `ResultComparator`, and I'm pretty sure it's not necessary. I've documented this bug with a TODO, which I'll address in a follow-up CL. Change-Id: Iba0169ded812d646b71966c2be22174d787c5a21 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/419424 Reviewed-by: Konstantin Shcheglov <[email protected]> Commit-Queue: Paul Berry <[email protected]>
1 parent b18aad8 commit d7fa12f

File tree

3 files changed

+366
-6
lines changed

3 files changed

+366
-6
lines changed

pkg/analyzer/test/src/fasta/recovery/recovery_test_support.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ class ResultComparator extends AstComparator {
8989
bool failIfNotNull(Object? first, Object? second) {
9090
if (second != null) {
9191
StringBuffer buffer = StringBuffer();
92-
buffer.write('Expected null; found a ');
93-
buffer.writeln(second.runtimeType);
92+
buffer.write('Expected a ');
93+
buffer.write(second.runtimeType);
94+
buffer.writeln('; found null');
9495
if (second is AstNode) {
9596
_safelyWriteNodePath(buffer, second);
9697
}
@@ -102,9 +103,8 @@ class ResultComparator extends AstComparator {
102103
@override
103104
bool failIsNull(Object first, Object? second) {
104105
StringBuffer buffer = StringBuffer();
105-
buffer.write('Expected a ');
106-
buffer.write(first.runtimeType);
107-
buffer.writeln('; found null');
106+
buffer.write('Expected null; found a ');
107+
buffer.writeln(first.runtimeType);
108108
if (first is AstNode) {
109109
_safelyWriteNodePath(buffer, first);
110110
}
@@ -115,7 +115,7 @@ class ResultComparator extends AstComparator {
115115
bool failRuntimeType(Object first, Object second) {
116116
StringBuffer buffer = StringBuffer();
117117
buffer.write('Expected a ');
118-
buffer.writeln(second.runtimeType);
118+
buffer.write(second.runtimeType);
119119
buffer.write('; found ');
120120
buffer.writeln(first.runtimeType);
121121
if (first is AstNode) {
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
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:analyzer/dart/ast/ast.dart';
6+
import 'package:analyzer/src/generated/parser.dart';
7+
import 'package:test/test.dart';
8+
import 'package:test_reflective_loader/test_reflective_loader.dart';
9+
10+
import '../../../generated/parser_test_base.dart';
11+
import 'recovery_test_support.dart';
12+
13+
main() {
14+
defineReflectiveSuite(() {
15+
defineReflectiveTests(ResultComparatorTest);
16+
});
17+
}
18+
19+
@reflectiveTest
20+
class ResultComparatorTest extends FastaParserTestCase {
21+
test_expectedIdentifier_anythingMatchesSynthetic() {
22+
// Any identifier matches a synthetic identifier.
23+
// TODO(paulberry): I don't think this behavior is intentional. Fix it.
24+
_assertMatched(
25+
actual: parseExpression('f(+x)', errors: [
26+
error(ParserErrorCode.MISSING_IDENTIFIER, 2, 1),
27+
]),
28+
expected: parseExpression('f(foo+x)'));
29+
}
30+
31+
test_expectedIdentifier_s_doesNotMatchNonSynthetic() {
32+
// `_s_` doesn't match non-synthetic identifiers.
33+
_assertMismatched(
34+
actual: parseExpression('f(a+x)'),
35+
expected: parseExpression('f(_s_+x)'),
36+
expectedFailureMessage: '''
37+
Expected: f(_s_ + x)
38+
Found: f(a + x)''');
39+
}
40+
41+
test_expectedIdentifier_s_matchesSynthetic() {
42+
// `_s_` matches a synthetic identifier.
43+
_assertMatched(
44+
actual: parseExpression('f(+x)', errors: [
45+
error(ParserErrorCode.MISSING_IDENTIFIER, 2, 1),
46+
]),
47+
expected: parseExpression('f(_s_+x)'));
48+
}
49+
50+
test_expectedNoOptionalNode_notPresent() {
51+
_assertMatched(
52+
actual: parseStatement('return;'), expected: parseStatement('return;'));
53+
}
54+
55+
test_expectedNoOptionalNode_present() {
56+
_assertMismatched(
57+
actual: parseStatement('return 0;'),
58+
expected: parseStatement('return;'),
59+
expectedFailureMessage: '''
60+
Expected null; found a IntegerLiteralImpl
61+
path: ReturnStatementImpl, IntegerLiteralImpl''');
62+
}
63+
64+
test_expectedOptionalNode_missing() {
65+
_assertMismatched(
66+
actual: parseStatement('return;'),
67+
expected: parseStatement('return 0;'),
68+
expectedFailureMessage: '''
69+
Expected a IntegerLiteralImpl; found null
70+
path: ReturnStatementImpl, IntegerLiteralImpl''');
71+
}
72+
73+
test_expectedOptionalNode_notMissing() {
74+
_assertMatched(
75+
actual: parseStatement('return 0;'),
76+
expected: parseStatement('return 0;'));
77+
}
78+
79+
test_expectedOptionalToken_missing() {
80+
_assertMismatched(
81+
actual: parseExpression('x is int'),
82+
expected: parseExpression('x is! int'),
83+
expectedFailureMessage: '''
84+
Expected a SimpleToken; found null
85+
''');
86+
}
87+
88+
test_expectedOptionalToken_notMissing() {
89+
_assertMatched(
90+
actual: parseExpression('x is! int'),
91+
expected: parseExpression('x is! int'));
92+
}
93+
94+
test_expectedToken_k_doesNotMatchNonKeywordToken() {
95+
// `_k_` doesn't match non-keyword tokens.
96+
_assertMismatched(
97+
actual: parseCompilationUnit('class C {}'),
98+
expected: parseCompilationUnit('class _k_ {}'),
99+
expectedFailureMessage: '''
100+
Expected: class _k_ {}
101+
Found: class C {}''');
102+
}
103+
104+
test_expectedToken_k_matchesKeywordToken() {
105+
// `_k_` matches a keyword token.
106+
_assertMatched(
107+
actual: parseCompilationUnit('class C { C(this); }', errors: [
108+
error(ParserErrorCode.EXPECTED_IDENTIFIER_BUT_GOT_KEYWORD, 12, 4),
109+
]),
110+
expected: parseCompilationUnit('class C { C(_k_); }'));
111+
}
112+
113+
test_expectedToken_randomIdentifierDoesNotMatchKeywordToken() {
114+
_assertMismatched(
115+
actual: parseCompilationUnit('class C { C(this); }', errors: [
116+
error(ParserErrorCode.EXPECTED_IDENTIFIER_BUT_GOT_KEYWORD, 12, 4),
117+
]),
118+
expected: parseCompilationUnit('class C { C(foo); }'),
119+
expectedFailureMessage: '''
120+
Expected: class C {C(foo);}
121+
Found: class C {C(this);}''');
122+
}
123+
124+
test_formalParameterList_namedParameters_matched() {
125+
_assertMatched(
126+
actual: parseFormalParameterList('(int? x, {int? y, int? z})'),
127+
expected: parseFormalParameterList('(int? x, {int? y, int? z})'));
128+
}
129+
130+
test_formalParameterList_namedParameters_mismatchedOpenBracketLocation() {
131+
_assertMismatched(
132+
actual: parseFormalParameterList('(int? x, {int? y, int? z})'),
133+
expected: parseFormalParameterList('(int? x, int? y, {int? z})'),
134+
expectedFailureMessage: '''
135+
Expected a SimpleFormalParameterImpl; found DefaultFormalParameterImpl
136+
path: FormalParameterListImpl, DefaultFormalParameterImpl''');
137+
}
138+
139+
test_formalParameterList_namedParameters_mismatchedParamKind() {
140+
_assertMismatched(
141+
actual: parseFormalParameterList('(int? x, {int? y, int? z})'),
142+
expected: parseFormalParameterList('(int? x, {int? y, int? z()})'),
143+
expectedFailureMessage: '''
144+
Expected a FunctionTypedFormalParameterImpl; found SimpleFormalParameterImpl
145+
path: FormalParameterListImpl, DefaultFormalParameterImpl, SimpleFormalParameterImpl''');
146+
}
147+
148+
test_formalParameterList_namedParameters_mismatchedParamName() {
149+
_assertMismatched(
150+
actual: parseFormalParameterList('(int? x, {int? y, int? z})'),
151+
expected: parseFormalParameterList('(int? x, {int? y, int? w})'),
152+
expectedFailureMessage: '''
153+
Expected: (int? x, {int? y, int? w})
154+
Found: (int? x, {int? y, int? z})''');
155+
}
156+
157+
test_formalParameterList_normalParameters_matched() {
158+
_assertMatched(
159+
actual: parseFormalParameterList('(int x, int y)'),
160+
expected: parseFormalParameterList('(int x, int y)'));
161+
}
162+
163+
test_formalParameterList_normalParameters_mismatchedParamKind() {
164+
_assertMismatched(
165+
actual: parseFormalParameterList('(int x, int y)'),
166+
expected: parseFormalParameterList('(int x, int y())'),
167+
expectedFailureMessage: '''
168+
Expected a FunctionTypedFormalParameterImpl; found SimpleFormalParameterImpl
169+
path: FormalParameterListImpl, SimpleFormalParameterImpl''');
170+
}
171+
172+
test_formalParameterList_normalParameters_mismatchedParamName() {
173+
_assertMismatched(
174+
actual: parseFormalParameterList('(int x, int y)'),
175+
expected: parseFormalParameterList('(int x, int z)'),
176+
expectedFailureMessage: '''
177+
Expected: (int x, int z)
178+
Found: (int x, int y)''');
179+
}
180+
181+
test_formalParameterList_optionalParameters_matched() {
182+
_assertMatched(
183+
actual: parseFormalParameterList('(int x, [int y, int z])'),
184+
expected: parseFormalParameterList('(int x, [int y, int z])'));
185+
}
186+
187+
test_formalParameterList_optionalParameters_mismatchedOpenBracketLocation() {
188+
_assertMismatched(
189+
actual: parseFormalParameterList('(int x, [int y, int z])'),
190+
expected: parseFormalParameterList('(int x, int y, [int z])'),
191+
expectedFailureMessage: '''
192+
Expected a SimpleFormalParameterImpl; found DefaultFormalParameterImpl
193+
path: FormalParameterListImpl, DefaultFormalParameterImpl''');
194+
}
195+
196+
test_formalParameterList_optionalParameters_mismatchedParamKind() {
197+
_assertMismatched(
198+
actual: parseFormalParameterList('(int x, [int y, int z])'),
199+
expected: parseFormalParameterList('(int x, [int y, int z()])'),
200+
expectedFailureMessage: '''
201+
Expected a FunctionTypedFormalParameterImpl; found SimpleFormalParameterImpl
202+
path: FormalParameterListImpl, DefaultFormalParameterImpl, SimpleFormalParameterImpl''');
203+
}
204+
205+
test_formalParameterList_optionalParameters_mismatchedParamName() {
206+
_assertMismatched(
207+
actual: parseFormalParameterList('(int x, [int y, int z])'),
208+
expected: parseFormalParameterList('(int x, [int y, int w])'),
209+
expectedFailureMessage: '''
210+
Expected: (int x, [int y, int w])
211+
Found: (int x, [int y, int z])''');
212+
}
213+
214+
test_mismatchedDocumentationComment_reference() {
215+
// Changes to comment references in documentation comments count as a
216+
// mismatch.
217+
_assertMismatched(actual: parseCompilationUnit('''
218+
/// A [Foo]
219+
class C {}
220+
'''), expected: parseCompilationUnit('''
221+
/// A [Bar]
222+
class C {}
223+
'''), expectedFailureMessage: '''
224+
Expected: class C {}
225+
Found: class C {}''');
226+
}
227+
228+
test_mismatchedDocumentationComment_text() {
229+
// Text-only changes to documentation comments don't count as a mismatch.
230+
// TODO(paulberry): consider changing this behavior.
231+
_assertMatched(actual: parseCompilationUnit('''
232+
/// A class
233+
class C {}
234+
'''), expected: parseCompilationUnit('''
235+
/// A clasx
236+
class C {}
237+
'''));
238+
}
239+
240+
test_mismatchedListLength_nodeList() {
241+
_assertMismatched(
242+
actual: parseExpression('f(x, y, z)'),
243+
expected: parseExpression('f(x, y, z, w)'),
244+
expectedFailureMessage: '''
245+
Expected a list of length 4
246+
[x, y, z, w]
247+
But found a list of length 3
248+
[x, y, z]
249+
path: MethodInvocationImpl, ArgumentListImpl''');
250+
}
251+
252+
test_mismatchedListLength_nonNodeList() {
253+
_assertMismatched(
254+
actual: parseExpression('#x.y.z'),
255+
expected: parseExpression('#x.y.z.w'),
256+
expectedFailureMessage: '''
257+
Expected a list of length 4
258+
[x, y, z, w]
259+
But found a list of length 3
260+
[x, y, z]
261+
''');
262+
}
263+
264+
test_mismatchedNodeType() {
265+
_assertMismatched(
266+
actual: parseExpression('f(x)'),
267+
expected: parseExpression('f(0)'),
268+
expectedFailureMessage: '''
269+
Expected a IntegerLiteralImpl; found SimpleIdentifierImpl
270+
path: MethodInvocationImpl, ArgumentListImpl, SimpleIdentifierImpl''');
271+
}
272+
273+
test_mismatchedToken() {
274+
_assertMismatched(
275+
actual: parseCompilationUnit('class C {}'),
276+
expected: parseCompilationUnit('class X {}'),
277+
expectedFailureMessage: '''
278+
Expected: class X {}
279+
Found: class C {}''');
280+
}
281+
282+
test_mismatchedTokenOffsets() {
283+
// Mismatched token offsets are tolerated.
284+
_assertMatched(
285+
actual: parseExpression('f( x)'), expected: parseExpression('f(x)'));
286+
}
287+
288+
test_syntheticEmptyString_matchesRegardlessOfQuoteType() {
289+
// A synthetic empty string matches an expectation of either `''` or `""`.
290+
_assertMatched(
291+
actual: parseCompilationUnit('export', errors: [
292+
error(ParserErrorCode.EXPECTED_TOKEN, 0, 6),
293+
error(ParserErrorCode.EXPECTED_STRING_LITERAL, 6, 0),
294+
]),
295+
expected: parseCompilationUnit("export '';"));
296+
_assertMatched(
297+
actual: parseCompilationUnit('export', errors: [
298+
error(ParserErrorCode.EXPECTED_TOKEN, 0, 6),
299+
error(ParserErrorCode.EXPECTED_STRING_LITERAL, 6, 0),
300+
]),
301+
expected: parseCompilationUnit('export "";'));
302+
}
303+
304+
void test_syntheticIdentifiers_matchRegardlessOfLexeme() {
305+
// Parser error recovery inserts a `FunctionDeclaration` whose `name` is a
306+
// synthetic token; that synthetic token's lexeme is based on the offset of
307+
// the extra `>` token. So the synthetic tokens produced for `f<T>> ...` and
308+
// `f<T> >...` are have different lexemes. But `ResultComparator` should
309+
// treat them as equivalent since they're both synthetic.
310+
_assertMatched(
311+
actual: parseCompilationUnit('''
312+
f<T>> () => null;
313+
''', errors: [
314+
error(ParserErrorCode.MISSING_FUNCTION_PARAMETERS, 0, 1),
315+
error(ParserErrorCode.MISSING_FUNCTION_BODY, 4, 1),
316+
error(ParserErrorCode.TOP_LEVEL_OPERATOR, 4, 1),
317+
]),
318+
expected: parseCompilationUnit('''
319+
f<T> >() => null;
320+
''', errors: [
321+
error(ParserErrorCode.MISSING_FUNCTION_PARAMETERS, 0, 1),
322+
error(ParserErrorCode.MISSING_FUNCTION_BODY, 5, 1),
323+
error(ParserErrorCode.TOP_LEVEL_OPERATOR, 5, 1),
324+
]));
325+
}
326+
327+
test_syntheticToken_matchesEquivalentNonSyntheticToken() {
328+
_assertMatched(
329+
actual: parseCompilationUnit('''
330+
mixin Foo implements
331+
''', errors: [
332+
error(ParserErrorCode.EXPECTED_TYPE_NAME, 21, 0),
333+
error(ParserErrorCode.EXPECTED_MIXIN_BODY, 21, 0),
334+
]),
335+
expected: parseCompilationUnit('''
336+
mixin Foo implements {}
337+
''', errors: [
338+
error(ParserErrorCode.EXPECTED_TYPE_NAME, 21, 1),
339+
]));
340+
}
341+
342+
void _assertMatched({required AstNode actual, required AstNode expected}) {
343+
ResultComparator.compare(actual, expected);
344+
}
345+
346+
void _assertMismatched(
347+
{required AstNode actual,
348+
required AstNode expected,
349+
required String expectedFailureMessage}) {
350+
try {
351+
ResultComparator.compare(actual, expected);
352+
} on TestFailure catch (e) {
353+
expect(e.message, expectedFailureMessage);
354+
return;
355+
}
356+
fail('Failed to find a mismatch');
357+
}
358+
}

0 commit comments

Comments
 (0)