blob: b049f535c1c2a69c20d1daea10edfe3c45a813ba [file] [log] [blame] [edit]
// Copyright (c) 2025, 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.
/// @docImport 'package:analyzer/dart/analysis/results.dart';
library;
import 'dart:convert' show json;
import 'package:analyzer/analysis_rule/analysis_rule.dart';
import 'package:analyzer/analysis_rule/pubspec.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/source/file_source.dart';
import 'package:analyzer/src/lint/pub.dart'; // ignore: implementation_imports
import 'package:analyzer/src/lint/registry.dart'; // ignore: implementation_imports
import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart';
import 'package:analyzer_testing/utilities/utilities.dart';
import 'package:meta/meta.dart';
ExpectedContextMessage contextMessage(
File file,
int offset,
int length, {
@Deprecated('Use textContains instead') String? text,
List<Pattern> textContains = const [],
}) {
assert(
text == null || textContains.isEmpty,
'Use only one of text or textContains',
);
if (text != null) {
textContains = [text];
}
return ExpectedContextMessage(
file,
offset,
length,
textContains: textContains,
);
}
/// Returns an [ExpectedDiagnostic] with the given arguments.
///
/// Just a short-named helper for use in the `assert*Diagnostics` methods.
ExpectedDiagnostic error(
DiagnosticCode code,
int offset,
int length, {
@Deprecated('Use messageContainsAll instead') Pattern? messageContains,
List<Pattern> messageContainsAll = const [],
Pattern? correctionContains,
List<ExpectedContextMessage>? contextMessages,
}) {
assert(
messageContains == null || messageContainsAll.isEmpty,
'Use only one of messageContains or messageContainsAll',
);
if (messageContains != null) {
messageContainsAll = [messageContains];
}
return ExpectedError(
code,
offset,
length,
messageContainsAll: messageContainsAll,
correctionContains: correctionContains,
contextMessages: contextMessages,
);
}
/// A base class for analysis rule tests that use test_reflective_loader.
abstract class AnalysisRuleTest extends PubPackageResolutionTest {
/// The [AbstractAnalysisRule] under test.
///
/// In a test class that extends [AnalysisRuleTest], this field must be set
/// from within [setUp], before calling `super.setUp`.
AbstractAnalysisRule rule = _SentinelRule();
/// The name of the analysis rule which this test is concerned with.
late String _analysisRule;
/// The name of the analysis rule which this test is concerned with.
// TODO(srawlins): In a major release, remove this getter, and implement
// `_analysisRule` as `=> rule.name;`.
@Deprecated("Set 'rule' in 'setUp' instead")
String get analysisRule => 'sentinel';
/// Asserts that no diagnostics are reported when resolving [content].
///
/// Note: Be sure to `await` any use of this API, to avoid stale analysis
/// results (See [DisposedAnalysisContextResult]).
Future<void> assertNoPubspecDiagnostics(String content) async {
newFile(testPackagePubspecPath, content);
var errors = await _analyzePubspecFile(content);
assertDiagnosticsIn(errors, []);
}
/// Asserts that [expectedDiagnostics] are reported when resolving [content].
///
/// Note: Be sure to `await` any use of this API, to avoid stale analysis
/// results (See [DisposedAnalysisContextResult]).
Future<void> assertPubspecDiagnostics(
String content,
List<ExpectedDiagnostic> expectedDiagnostics,
) async {
newFile(testPackagePubspecPath, content);
var errors = await _analyzePubspecFile(content);
assertDiagnosticsIn(errors, expectedDiagnostics);
}
@override
String correctionMessage(List<Diagnostic> diagnostics) {
var buffer = StringBuffer();
diagnostics.sort((first, second) => first.offset.compareTo(second.offset));
buffer.writeln();
buffer.writeln('To accept the current state, expect:');
for (var actual in diagnostics) {
if (actual.diagnosticCode is LintCode) {
buffer.write(' lint(');
} else {
buffer.write(' error(${actual.diagnosticCode}, ');
}
buffer.write('${actual.offset}, ${actual.length}');
if (actual.diagnosticCode.name != _analysisRule) {
buffer.write(", name: '${actual.diagnosticCode.name}'");
}
buffer.writeln('),');
}
return buffer.toString();
}
/// Returns an "expected diagnostic" for [rule] (or [name], if given)
/// at [offset] and [length].
///
/// If given, [messageContains] is used to match against a diagnostic's
/// message, and [correctionContains] is used to match against a diagnostic's
/// correction message.
ExpectedDiagnostic lint(
int offset,
int length, {
Pattern? correctionContains,
@Deprecated('Use messageContainsAll instead') Pattern? messageContains,
List<Pattern> messageContainsAll = const [],
String? name,
List<ExpectedContextMessage>? contextMessages,
}) {
assert(
messageContains == null || messageContainsAll.isEmpty,
'Use only one of messageContains or messageContainsAll',
);
if (messageContains != null) {
messageContainsAll = [messageContains];
}
return ExpectedLint(
name ?? _analysisRule,
offset,
length,
messageContainsAll: messageContainsAll,
correctionContains: correctionContains,
contextMessages: contextMessages,
);
}
@mustCallSuper
@override
void setUp() {
// TODO(srawlins): In a major release, change this logic to just ensure that
// `rule` has been set to an analysis rule instance other than
// `_SentinelRule`.
if (rule is _SentinelRule) {
if (analysisRule == 'sentinel') {
throw StateError('The `rule` field must be set in the `setUp` method.');
}
// The developer is using the deprecated `analysisRule` to set the
// rule-under-test.
_analysisRule = analysisRule;
} else {
_analysisRule = rule.name;
Registry.ruleRegistry.registerLintRule(rule);
}
super.setUp();
newAnalysisOptionsYamlFile(
testPackageRootPath,
analysisOptionsContent(experiments: experiments, rules: [_analysisRule]),
);
}
@override
String unexpectedMessage(List<Diagnostic> unmatchedActual) {
var buffer = StringBuffer();
if (buffer.isNotEmpty) {
buffer.writeln();
}
buffer.writeln('Found but did not expect:');
for (var actual in unmatchedActual) {
buffer.write(' $_analysisRule.${actual.diagnosticCode.name} [');
buffer.write('${actual.offset}, ${actual.length}, ${actual.message}');
if (actual.correctionMessage case Pattern correctionMessage) {
buffer.write(', ');
buffer.write(json.encode(correctionMessage));
}
buffer.writeln(']');
}
return buffer.toString();
}
Future<List<Diagnostic>> _analyzePubspecFile(String content) async {
var path = convertPath(testPackagePubspecPath);
var pubspecRules = <AbstractAnalysisRule, PubspecVisitor<Object?>>{};
var rules = Registry.ruleRegistry.where((r) => _analysisRule == r.name);
for (var rule in rules) {
var visitor = rule.pubspecVisitor;
if (visitor != null) {
pubspecRules[rule] = visitor;
}
}
if (pubspecRules.isEmpty) {
throw UnsupportedError(
'Resolving pubspec files only supported with rules with '
'PubspecVisitors.',
);
}
var sourceUri = resourceProvider.pathContext.toUri(path);
var pubspecAst = Pubspec.parse(
content,
sourceUrl: sourceUri,
resourceProvider: resourceProvider,
);
var listener = RecordingDiagnosticListener();
var file = resourceProvider.getFile(path);
var reporter = DiagnosticReporter(listener, FileSource(file, sourceUri));
for (var entry in pubspecRules.entries) {
entry.key.reporter = reporter;
pubspecAst.accept(entry.value);
}
return [...listener.diagnostics];
}
}
/// A sentinel [AnalysisRule] for use while [AnalysisRuleTest.analysisRule] is
/// still available but deprecated, and using it's replacement,
/// [AnalysisRuleTest.rule], is not yet mandatory.
final class _SentinelRule extends AnalysisRule {
_SentinelRule() : super(name: 'sentinel', description: 'sentinel');
@override
dynamic noSuchMethod(invocation) => super.noSuchMethod(invocation);
}