blob: 6b4ac421265d7e9bc7f0190c566fc228128e39e3 [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// 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/dart/analysis/features.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/src/dart/analysis/experiments.dart';
import 'package:analyzer_utilities/message_examples.dart';
import 'package:analyzer_utilities/messages.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'src/dart/resolution/context_collection_resolution.dart';
main() {
defineReflectiveSuite(() {
defineReflectiveTests(VerifySharedMessageExamplesTest);
});
}
/// Verifies that the analyzer reports the expected diagnostic for each of the
/// examples in `pkg/_fe_analyzer_shared/messages.yaml`.
///
/// These are the same examples that the front end checks in
/// `pkg/front_end/test/messages_suite.dart`, and they're checked in a similar
/// way: each example must cause the diagnostic to be reported exactly once
/// (unless `exampleAllowMultipleReports` is set), and must not cause any other
/// errors to be reported (unless `exampleAllowOtherCodes` is set). Unlike the
/// front end, diagnostics that aren't errors (such as warnings and lints) are
/// ignored, unless they're the diagnostic being tested.
@reflectiveTest
class VerifySharedMessageExamplesTest {
/// Examples that cause an assertion failure in the analyzer, mapped to the
/// reason why.
///
/// Keys have the same form as in [knownFailures].
///
/// When assertions are enabled, these examples are treated like the ones in
/// [knownFailures]. When assertions are disabled, they're expected to pass.
static const Map<String, String> knownAssertionFailures = {};
/// Examples that are known not to work with the analyzer, mapped to the
/// reason why.
///
/// Keys are of the form `<messageName>/<exampleName>`, where
/// `<messageName>` is the key of the message in `messages.yaml`, and
/// `<exampleName>` is the name of the example (e.g. `script` or
/// `script2`).
///
/// If an example listed here starts working, the test fails, so that the
/// entry can be removed.
static const Map<String, String> knownFailures = {
'constAndFinal/declaration3':
'Parser recovery reports missing_identifier instead (the CFE has '
'the same problem)',
'constAndFinal/declaration4':
'Parser recovery reports missing_identifier instead (the CFE has '
'the same problem)',
'constructorWithTypeArguments/script1':
'Analyzer reports wrong_number_of_type_arguments_constructor instead',
'constructorWithTypeArguments/script2':
'Analyzer reports wrong_number_of_type_arguments_constructor instead',
'constructorWithWrongName/script2':
'Analyzer reports invalid_factory_name_not_a_class instead',
'constructorWithWrongName/script3':
'Analyzer reports invalid_factory_name_not_a_class instead',
'expectedAnInitializer/script1':
'Analyzer also reports initializer_for_non_existent_field',
'expectedStatement/statement':
'Analyzer reports missing_identifier instead',
'experimentNotEnabledOffByDefault/script':
'Analyzer reports experiment_not_enabled instead',
'extensionAugmentationHasOnClause/script':
'Analyzer parses `augment` as a type name (the CFE has the same '
'problem)',
'externalMethodWithBody/script2': 'Analyzer reports nothing',
'externalMethodWithBody/script3': 'Analyzer reports nothing',
'illegalAssignmentToNonAssignable/script1':
'Analyzer also reports undefined_function',
'initializingDeclaringParameter/script':
'Analyzer also reports initializing_formal_for_non_existent_field',
'invalidInitializer/script': 'Analyzer reports nothing',
'metadataTypeArguments/script':
'Analyzer reports experiment_not_enabled instead',
'metadataTypeArgumentsUninstantiated/script1':
'Analyzer also reports disallowed_type_instantiation_expression',
'missingAssignableSelector/script1':
'Analyzer also reports undefined_function',
'missingPrimaryConstructor/script':
'Analyzer also reports expected_representation_field',
'missingPrimaryConstructorParameters/script':
'Analyzer also reports expected_representation_field',
'partOfTwice/script1': 'Analyzer also reports part_of_different_library',
'typeArgumentsOnTypeVariable/script1':
'Analyzer reports wrong_number_of_type_arguments instead',
};
@TestTimeout(Timeout.factor(4))
test_examples() async {
var assertionsEnabled = false;
assert(assertionsEnabled = true);
var problems = <String>[];
var knownFailuresSeen = <String>{};
for (var message in feAnalyzerSharedMessages) {
if (message.isRemoved) continue;
var expectedCode =
(message.sharedName ?? message.analyzerCode).snakeCaseName;
for (var example in message.examples) {
// TODO(paulberry): consider supporting `bytes` examples.
if (example is! SourceExample) continue;
var id = '${message.keyString}/${example.name}';
var problem = await _checkExample(message, example, expectedCode);
var isKnownAssertionFailure = knownAssertionFailures.containsKey(id);
if (knownFailures.containsKey(id) || isKnownAssertionFailure) {
knownFailuresSeen.add(id);
}
if (knownFailures.containsKey(id) ||
(isKnownAssertionFailure && assertionsEnabled)) {
if (problem == null) {
problems.add(
'$id: passes, but is listed as a known failure. Please remove '
'it from the list.',
);
}
} else if (problem != null) {
problems.add('$id: $problem');
}
}
}
for (var id in [...knownFailures.keys, ...knownAssertionFailures.keys]) {
if (!knownFailuresSeen.contains(id)) {
problems.add(
'$id: listed as a known failure, but no such example exists. Please '
'remove it from the list.',
);
}
}
if (problems.isNotEmpty) {
fail(problems.join('\n\n'));
}
}
/// Analyzes the given [files] (a map from file name to contents) with the
/// given [experimentalFeatures] enabled, and returns all the diagnostics
/// that are reported.
Future<List<Diagnostic>> _analyze(
Map<String, String> files, {
required List<Feature> experimentalFeatures,
}) async {
var test = _ExampleTest(experimentalFeatures);
test.setUp();
try {
var diagnostics = <Diagnostic>[];
for (var MapEntry(key: fileName, value: content) in files.entries) {
test.newFile('${test.testPackageLibPath}/$fileName', content);
}
for (var fileName in files.keys) {
var result = await test.resolveFile2(
test.getFile('${test.testPackageLibPath}/$fileName'),
);
diagnostics.addAll(result.diagnostics);
}
return diagnostics;
} finally {
await test.tearDown();
}
}
/// Analyzes [example] and checks that it produces the diagnostic named
/// [expectedCode], subject to the options specified in [message].
///
/// Returns `null` if the example behaves as expected; otherwise returns a
/// description of the problem. An exception thrown during analysis
/// (including an assertion failure) is treated as a problem.
Future<String?> _checkExample(
SharedMessage message,
SourceExample example,
String expectedCode,
) async {
List<Diagnostic> diagnostics;
try {
diagnostics = await _analyze(
example.files,
experimentalFeatures: [
for (var experiment in message.experiments)
if (ExperimentStatus.knownFeatures[experiment] case var feature?
when !feature.isEnabledByDefault)
feature,
],
);
} catch (e, stackTrace) {
// Report the exception (which may be an assertion failure) as a problem
// with this example, so that the other examples are still checked.
return 'Exception during analysis: $e\n$stackTrace';
}
var expected = <Diagnostic>[];
var unexpected = <Diagnostic>[];
for (var diagnostic in diagnostics) {
if (diagnostic.diagnosticCode.lowerCaseName == expectedCode) {
expected.add(diagnostic);
} else if (diagnostic.severity == Severity.error) {
unexpected.add(diagnostic);
}
}
if (message.exampleAllowOtherCodes) {
unexpected.clear();
}
var expectedCount =
message.exampleAllowMultipleReports && expected.isNotEmpty
? 1
: expected.length;
if (expectedCount == 1 && unexpected.isEmpty) {
return null;
}
var description = switch ((expectedCount, unexpected.isEmpty)) {
(0, true) => 'No diagnostic reported',
(0, false) => 'Got only unrelated errors',
(1, false) =>
'Got the expected diagnostic, but also other errors. Maybe add '
'`exampleAllowOtherCodes: true`.',
(_, true) =>
'Expected diagnostic reported multiple times. Maybe add '
'`exampleAllowMultipleReports: true`.',
(_, false) =>
'Expected diagnostic reported multiple times, and also other errors.',
};
var buffer = StringBuffer('$description (expected $expectedCode).');
for (var diagnostic in [...expected, ...unexpected]) {
buffer.write(
'\n ${diagnostic.diagnosticCode.lowerCaseName} '
'(${diagnostic.source.shortName}:${diagnostic.offset}): '
'${diagnostic.message}',
);
}
return buffer.toString();
}
}
/// A test class that creates an environment suitable for analyzing a single
/// example.
class _ExampleTest extends PubPackageResolutionTest {
@override
final List<Feature> experimentalFeatures;
new(this.experimentalFeatures);
}