CQ. Switch analysis options tests to inline expected diagnostics.

Change-Id: I2c33dbd645bd8611836cbcbab597a23165d1fb9e
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/511060
Reviewed-by: Johnni Winther <johnniwinther@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
Konstantin Shcheglov
2026-06-11 07:45:07 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 6de2588d56
commit 649a56d6e4
8 changed files with 847 additions and 1153 deletions
@@ -176,6 +176,36 @@ class NodeTextExpectationsCollector {
methodName: 'parseTestCodeWithDiagnostics',
argument: _ArgumentIndex(0),
),
_AssertMethod(
className: 'AbstractAnalysisOptionsTest',
methodName: 'assertDiagnosticsInCode',
argument: _ArgumentIndex(0),
),
_AssertMethod(
className: 'AbstractAnalysisOptionsTest',
methodName: 'assertDiagnosticsInFiles',
argument: _ArgumentMapEntryValue(mapArgument: _ArgumentIndex(0)),
),
_AssertMethod(
className: 'OptionsFileValidatorTest',
methodName: 'validate',
argument: _ArgumentIndex(0),
),
_AssertMethod(
className: 'OptionsProviderTest',
methodName: 'assertDiagnosticsInOptionsFile',
argument: _ArgumentIndex(1),
),
_AssertMethod(
className: 'OptionsRuleValidatorTestMixin',
methodName: 'assertDiagnostics',
argument: _ArgumentIndex(0),
),
_AssertMethod(
className: 'OptionsRuleValidatorTestMixin',
methodName: 'assertRuleDiagnosticsInFiles',
argument: _ArgumentMapEntryValue(mapArgument: _ArgumentIndex(0)),
),
_AssertMethod(
className: 'ResolutionTest',
methodName: 'assertDartObjectText',
@@ -2,7 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:_fe_analyzer_shared/src/base/errors.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/source/file_source.dart';
import 'package:analyzer/src/analysis_options/options_file_validator.dart';
@@ -12,59 +12,46 @@ import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/source/package_map_resolver.dart';
import 'package:analyzer/src/test_utilities/lint_registration_mixin.dart';
import 'package:analyzer_testing/resource_provider_mixin.dart';
import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart';
import 'package:analyzer_testing/src/expected_diagnostics.dart';
import 'package:meta/meta.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:test/test.dart';
import '../../../generated/test_support.dart';
import '../../../util/diff.dart';
import '../../dart/resolution/node_text_expectations.dart';
abstract class AbstractAnalysisOptionsTest
with ResourceProviderMixin, LintRegistrationMixin {
with
ResourceProviderMixin,
LintRegistrationMixin,
AnalysisOptionsDiagnosticExpectationMixin {
late SourceFactory sourceFactory;
Map<String, String>? dependencies;
late File analysisOptionsFile = newFile(analysisOptionsPath, '');
late String analysisOptionsPath = convertPath('/analysis_options.yaml');
late File analysisOptionsFile = getFile('/analysis_options.yaml');
VersionConstraint? get sdkVersionConstraint => null;
Future<void> assertErrorsInCode(
String code,
List<ExpectedDiagnostic> expectedDiagnostics,
) async {
analysisOptionsFile.writeAsStringSync(code);
Future<void> assertDiagnosticsInCode(String code) async {
await assertDiagnosticsInFiles({analysisOptionsFile: code});
}
Future<void> assertDiagnosticsInFiles(Map<File, String> codeByFile) async {
var cleanCodeByFile = writeFilesWithoutDiagnosticExpectations(codeByFile);
var diagnostics = AnalysisOptionsAnalyzer(
initialSource: FileSource(analysisOptionsFile),
sourceFactory: sourceFactory,
contextRoot: '/',
sdkVersionConstraint: sdkVersionConstraint,
resourceProvider: resourceProvider,
).walkIncludes(content: code);
var diagnosticListener = GatheringDiagnosticListener();
diagnosticListener.addAll(diagnostics);
diagnosticListener.assertErrors(expectedDiagnostics);
).walkIncludes(content: cleanCodeByFile[analysisOptionsFile]!);
assertDiagnosticMarkersInFiles(
codeByFile: codeByFile,
diagnostics: diagnostics,
);
}
Future<void> assertNoErrorsInCode(String code) async =>
await assertErrorsInCode(code, const []);
ExpectedError error(
DiagnosticCode code,
int offset,
int length, {
Pattern? correctionContains,
String? text,
List<Pattern> messageContains = const [],
List<ExpectedContextMessage> contextMessages =
const <ExpectedContextMessage>[],
}) => ExpectedError(
code,
offset,
length,
correctionContains: correctionContains,
messageContainsAll: messageContains,
contextMessages: contextMessages,
);
void setUp() {
var resolvers = [
ResourceUriResolver(resourceProvider),
@@ -82,3 +69,79 @@ abstract class AbstractAnalysisOptionsTest
unregisterLintRules();
}
}
/// Shared inline diagnostic expectation checks for analysis-options tests.
///
/// These helpers only compare diagnostics with inline markers. Test-specific
/// helpers remain responsible for choosing which analyzer or validator entry
/// point produces the diagnostics.
mixin AnalysisOptionsDiagnosticExpectationMixin {
void assertDiagnosticMarkersInFiles({
required Map<File, String> codeByFile,
required List<Diagnostic> diagnostics,
}) {
var cleanCodeByFile = {
for (var entry in codeByFile.entries)
entry.key: removeDiagnosticExpectations(entry.value),
};
var actualCodeByFile = updateExpectedDiagnosticsForFiles(
contentByFile: cleanCodeByFile,
actualDiagnosticsByFile: _diagnosticsByFile(
files: codeByFile.keys,
diagnostics: diagnostics,
),
);
var hasMismatch = false;
var index = 0;
for (var entry in codeByFile.entries) {
var actual = actualCodeByFile[entry.key]!;
if (actual != entry.value) {
NodeTextExpectationsCollector.add(actual, intraInvocationId: '$index');
print('-------- ${entry.key.path} --------');
printPrettyDiff(entry.value, actual);
hasMismatch = true;
}
index++;
}
if (hasMismatch) {
fail('See the difference above.');
}
}
Map<File, String> writeFilesWithoutDiagnosticExpectations(
Map<File, String> codeByFile,
) {
var cleanCodeByFile = {
for (var entry in codeByFile.entries)
entry.key: removeDiagnosticExpectations(entry.value),
};
for (var entry in cleanCodeByFile.entries) {
entry.key.writeAsStringSync(entry.value);
}
return cleanCodeByFile;
}
Map<File, List<Diagnostic>> _diagnosticsByFile({
required Iterable<File> files,
required List<Diagnostic> diagnostics,
}) {
var fileByPath = {for (var file in files) file.path: file};
var diagnosticsByFile = {for (var file in files) file: <Diagnostic>[]};
for (var diagnostic in diagnostics) {
var filePath = diagnostic.problemMessage.filePath;
var file = fileByPath[filePath];
if (file == null) {
fail(
'Cannot generate diagnostic expectations for $filePath: '
'no content was provided.',
);
}
diagnosticsByFile[file]!.add(diagnostic);
}
return diagnosticsByFile;
}
}
@@ -2,7 +2,6 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'analysis_options_test_support.dart';
@@ -16,159 +15,107 @@ main() {
@reflectiveTest
class IncludeFileNotFoundTest extends AbstractAnalysisOptionsTest {
Future<void> test_notFound_existent_doubleQuoted() async {
await assertErrorsInCode(
'''
await assertDiagnosticsInCode('''
include: "./analysis_options.yaml"
''',
[error(diag.recursiveIncludeFile, 9, 25)],
);
// ^^^^^^^^^^^^^^^^^^^^^^^^^
// [diag.recursiveIncludeFile] The URI './analysis_options.yaml' included in '/analysis_options.yaml' includes '/analysis_options.yaml', creating a circular reference.
''');
}
Future<void> test_notFound_existent_list_first() async {
newFile('/included1.yaml', '');
await assertErrorsInCode(
'''
await assertDiagnosticsInFiles({
analysisOptionsFile: '''
include:
- ./analysis_options.yaml
// ^^^^^^^^^^^^^^^^^^^^^^^
// [diag.recursiveIncludeFile] The URI './analysis_options.yaml' included in '/analysis_options.yaml' includes '/analysis_options.yaml', creating a circular reference.
- included1.yaml
''',
[error(diag.recursiveIncludeFile, 13, 23)],
);
getFile('/included1.yaml'): '',
});
}
Future<void> test_notFound_existent_list_second() async {
newFile('/included1.yaml', '');
await assertErrorsInCode(
'''
await assertDiagnosticsInFiles({
analysisOptionsFile: '''
include:
- included1.yaml
- ./analysis_options.yaml
// ^^^^^^^^^^^^^^^^^^^^^^^
// [diag.recursiveIncludeFile] The URI './analysis_options.yaml' included in '/analysis_options.yaml' includes '/analysis_options.yaml', creating a circular reference.
''',
[error(diag.recursiveIncludeFile, 32, 23)],
);
getFile('/included1.yaml'): '',
});
}
Future<void> test_notFound_existent_notQuoted() async {
await assertErrorsInCode(
'''
await assertDiagnosticsInCode('''
include: ./analysis_options.yaml
''',
[error(diag.recursiveIncludeFile, 9, 23)],
);
// ^^^^^^^^^^^^^^^^^^^^^^^
// [diag.recursiveIncludeFile] The URI './analysis_options.yaml' included in '/analysis_options.yaml' includes '/analysis_options.yaml', creating a circular reference.
''');
}
Future<void> test_notFound_existent_singleQuoted() async {
await assertErrorsInCode(
'''
await assertDiagnosticsInCode('''
include: './analysis_options.yaml'
''',
[error(diag.recursiveIncludeFile, 9, 25)],
);
// ^^^^^^^^^^^^^^^^^^^^^^^^^
// [diag.recursiveIncludeFile] The URI './analysis_options.yaml' included in '/analysis_options.yaml' includes '/analysis_options.yaml', creating a circular reference.
''');
}
Future<void> test_notFound_nonexistent_doubleQuoted() async {
await assertErrorsInCode(
'''
await assertDiagnosticsInCode('''
# We don't depend on pedantic, but we should consider adding it.
include: "package:pedantic/analysis_options.yaml"
''',
[
error(
diag.includeFileNotFound,
74,
40,
text:
"The URI 'package:pedantic/analysis_options.yaml' included in "
"'${convertPath('/analysis_options.yaml')}' can't be found "
"when analyzing '/'.",
),
],
);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// [diag.includeFileNotFound] The URI 'package:pedantic/analysis_options.yaml' included in '/analysis_options.yaml' can't be found when analyzing '/'.
''');
}
Future<void> test_notFound_nonexistent_list_first() async {
newFile('/included1.yaml', '');
await assertErrorsInCode(
'''
await assertDiagnosticsInFiles({
analysisOptionsFile: '''
# We don't depend on pedantic, but we should consider adding it.
include:
- package:pedantic/analysis_options.yaml
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// [diag.includeFileNotFound] The URI 'package:pedantic/analysis_options.yaml' included in '/analysis_options.yaml' can't be found when analyzing '/'.
- included1.yaml
''',
[
error(
diag.includeFileNotFound,
78,
38,
text:
"The URI 'package:pedantic/analysis_options.yaml' included in "
"'${convertPath('/analysis_options.yaml')}' can't be found "
"when analyzing '/'.",
),
],
);
getFile('/included1.yaml'): '',
});
}
Future<void> test_notFound_nonexistent_list_second() async {
newFile('/included1.yaml', '');
await assertErrorsInCode(
'''
await assertDiagnosticsInFiles({
analysisOptionsFile: '''
# We don't depend on pedantic, but we should consider adding it.
include:
- included1.yaml
- package:pedantic/analysis_options.yaml
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// [diag.includeFileNotFound] The URI 'package:pedantic/analysis_options.yaml' included in '/analysis_options.yaml' can't be found when analyzing '/'.
''',
[
error(
diag.includeFileNotFound,
97,
38,
text:
"The URI 'package:pedantic/analysis_options.yaml' included in "
"'${convertPath('/analysis_options.yaml')}' can't be found "
"when analyzing '/'.",
),
],
);
getFile('/included1.yaml'): '',
});
}
Future<void> test_notFound_nonexistent_notQuoted() async {
await assertErrorsInCode(
'''
await assertDiagnosticsInCode('''
# We don't depend on pedantic, but we should consider adding it.
include: package:pedantic/analysis_options.yaml
''',
[
error(
diag.includeFileNotFound,
74,
38,
text:
"The URI 'package:pedantic/analysis_options.yaml' included in "
"'${convertPath('/analysis_options.yaml')}' can't be found "
"when analyzing '/'.",
),
],
);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// [diag.includeFileNotFound] The URI 'package:pedantic/analysis_options.yaml' included in '/analysis_options.yaml' can't be found when analyzing '/'.
''');
}
Future<void> test_notFound_nonexistent_singleQuoted() async {
await assertErrorsInCode(
'''
await assertDiagnosticsInCode('''
# We don't depend on pedantic, but we should consider adding it.
include: 'package:pedantic/analysis_options.yaml'
''',
[
error(
diag.includeFileNotFound,
74,
40,
text:
"The URI 'package:pedantic/analysis_options.yaml' included in "
"'${convertPath('/analysis_options.yaml')}' can't be found "
"when analyzing '/'.",
),
],
);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// [diag.includeFileNotFound] The URI 'package:pedantic/analysis_options.yaml' included in '/analysis_options.yaml' can't be found when analyzing '/'.
''');
}
}
@@ -2,7 +2,6 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'analysis_options_test_support.dart';
@@ -16,25 +15,16 @@ main() {
@reflectiveTest
class IncludeFileWarningTest extends AbstractAnalysisOptionsTest {
Future<void> test_fileWarning() async {
newFile('/a.yaml', '''
await assertDiagnosticsInFiles({
analysisOptionsFile: '''
include: a.yaml
// ^^^^^^
// [diag.includedFileWarning] Warning in the included options file /a.yaml(12..20): The option 'something' isn't supported by 'analyzer'.
''',
getFile('/a.yaml'): '''
analyzer:
something: bad
''');
await assertErrorsInCode(
'''
include: a.yaml
''',
[
error(
diag.includedFileWarning,
9,
6,
messageContains: [
'Warning in the included options file ${convertPath('/a.yaml')}',
": The option 'something' isn't supported by 'analyzer'.",
],
),
],
);
});
}
}
@@ -2,7 +2,6 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'analysis_options_test_support.dart';
@@ -16,203 +15,135 @@ main() {
@reflectiveTest
class RecursiveIncludeFileTest extends AbstractAnalysisOptionsTest {
Future<void> test_itself() async {
await assertErrorsInCode(
'''
await assertDiagnosticsInCode('''
include: analysis_options.yaml
''',
[
error(
diag.recursiveIncludeFile,
9,
21,
text:
"The URI 'analysis_options.yaml' included in "
"'${convertPath('/analysis_options.yaml')}' includes "
"'${convertPath('/analysis_options.yaml')}', "
"creating a circular reference.",
),
],
);
// ^^^^^^^^^^^^^^^^^^^^^
// [diag.recursiveIncludeFile] The URI 'analysis_options.yaml' included in '/analysis_options.yaml' includes '/analysis_options.yaml', creating a circular reference.
''');
}
Future<void> test_itself_inList() async {
await assertErrorsInCode(
'''
await assertDiagnosticsInCode('''
include:
- analysis_options.yaml
''',
[
error(
diag.recursiveIncludeFile,
13,
21,
text:
"The URI 'analysis_options.yaml' included in "
"'${convertPath('/analysis_options.yaml')}' includes "
"'${convertPath('/analysis_options.yaml')}', "
"creating a circular reference.",
),
],
);
// ^^^^^^^^^^^^^^^^^^^^^
// [diag.recursiveIncludeFile] The URI 'analysis_options.yaml' included in '/analysis_options.yaml' includes '/analysis_options.yaml', creating a circular reference.
''');
}
Future<void> test_notRecursive() async {
newFile('/a.yaml', '''
include: b.yaml
''');
newFile('/b.yaml', '');
await assertNoErrorsInCode('''
await assertDiagnosticsInFiles({
analysisOptionsFile: '''
include:
- a.yaml
- b.yaml
''');
''',
getFile('/a.yaml'): '''
include: b.yaml
''',
getFile('/b.yaml'): '',
});
}
Future<void> test_notRecursive_included() async {
newFile('/a.yaml', '''
await assertDiagnosticsInFiles({
analysisOptionsFile: '''
include: c.yaml
''',
getFile('/a.yaml'): '''
include: b.yaml
''');
newFile('/b.yaml', '');
newFile('/c.yaml', '''
''',
getFile('/b.yaml'): '',
getFile('/c.yaml'): '''
include:
- a.yaml
- b.yaml
''');
await assertNoErrorsInCode('''
include: c.yaml
''');
''',
});
}
Future<void> test_recursive() async {
newFile('/a.yaml', '''
include: b.yaml
''');
newFile('/b.yaml', '''
include: analysis_options.yaml
''');
await assertErrorsInCode(
'''
await assertDiagnosticsInFiles({
analysisOptionsFile: '''
include: a.yaml
// ^^^^^^
// [diag.recursiveIncludeFile] The URI 'analysis_options.yaml' included in '/b.yaml' includes '/b.yaml', creating a circular reference.
''',
[
error(
diag.recursiveIncludeFile,
9,
6,
text:
"The URI 'analysis_options.yaml' included in "
"'${convertPath('/b.yaml')}' includes "
"'${convertPath('/b.yaml')}', "
"creating a circular reference.",
),
],
);
getFile('/a.yaml'): '''
include: b.yaml
''',
getFile('/b.yaml'): '''
include: analysis_options.yaml
''',
});
}
Future<void> test_recursive_itself() async {
newFile('/a.yaml', '''
await assertDiagnosticsInFiles({
analysisOptionsFile: '''
include: a.yaml
''');
await assertErrorsInCode(
'''
// ^^^^^^
// [diag.includedFileWarning] Warning in the included options file /a.yaml(9..14): The file includes itself recursively.
''',
getFile('/a.yaml'): '''
include: a.yaml
''',
[
error(
diag.includedFileWarning,
9,
6,
messageContains: [
"Warning in the included options file ${convertPath('/a.yaml')}",
": The file includes itself recursively.",
],
),
],
);
});
}
Future<void> test_recursive_listAtTop() async {
newFile('/a.yaml', '''
include: b.yaml
''');
newFile('/b.yaml', '''
include: analysis_options.yaml
''');
newFile('/empty.yaml', '''
''');
await assertErrorsInCode(
'''
await assertDiagnosticsInFiles({
analysisOptionsFile: '''
include:
- empty.yaml
- a.yaml
// ^^^^^^
// [diag.recursiveIncludeFile] The URI 'analysis_options.yaml' included in '/b.yaml' includes '/b.yaml', creating a circular reference.
''',
[
error(
diag.recursiveIncludeFile,
28,
6,
text:
"The URI 'analysis_options.yaml' included in "
"'${convertPath('/b.yaml')}' includes "
"'${convertPath('/b.yaml')}', "
"creating a circular reference.",
),
],
);
getFile('/a.yaml'): '''
include: b.yaml
''',
getFile('/b.yaml'): '''
include: analysis_options.yaml
''',
getFile('/empty.yaml'): '''
''',
});
}
Future<void> test_recursive_listIncluded() async {
newFile('/a.yaml', '''
await assertDiagnosticsInFiles({
analysisOptionsFile: '''
include: a.yaml
// ^^^^^^
// [diag.recursiveIncludeFile] The URI 'analysis_options.yaml' included in '/b.yaml' includes '/b.yaml', creating a circular reference.
''',
getFile('/a.yaml'): '''
include:
- empty.yaml
- b.yaml
''');
newFile('/b.yaml', '''
include: analysis_options.yaml
''');
newFile('/empty.yaml', '''
''');
await assertErrorsInCode(
'''
include: a.yaml
''',
[
error(
diag.recursiveIncludeFile,
9,
6,
text:
"The URI 'analysis_options.yaml' included in "
"'${convertPath('/b.yaml')}' includes "
"'${convertPath('/b.yaml')}', "
"creating a circular reference.",
),
],
);
getFile('/b.yaml'): '''
include: analysis_options.yaml
''',
getFile('/empty.yaml'): '''
''',
});
}
Future<void> test_recursive_notInBeginning() async {
newFile('/a.yaml', '''
include: b.yaml
''');
newFile('/b.yaml', '''
await assertDiagnosticsInFiles({
analysisOptionsFile: '''
include: a.yaml
''');
await assertErrorsInCode(
'''
// ^^^^^^
// [diag.includedFileWarning] Warning in the included options file /a.yaml(9..14): The file includes itself recursively.
''',
getFile('/a.yaml'): '''
include: b.yaml
''',
getFile('/b.yaml'): '''
include: a.yaml
''',
[
error(
diag.includedFileWarning,
9,
6,
messageContains: [
"Warning in the included options file ${convertPath('/a.yaml')}",
": The file includes itself recursively.",
],
),
],
);
});
}
}
@@ -4,6 +4,7 @@
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../../dart/resolution/node_text_expectations.dart';
import 'include_file_not_found_test.dart' as include_file_not_found;
import 'include_file_warning_test.dart' as include_file_warning;
import 'recursive_include_file_test.dart' as recursive_include_file;
@@ -13,5 +14,6 @@ main() {
include_file_not_found.main();
include_file_warning.main();
recursive_include_file.main();
defineReflectiveTests(UpdateNodeTextExpectations);
});
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff