blob: 4aa77e9f6422b3bbc91cbd6b7f5627ea16380c5f [file] [log] [blame] [edit]
// Copyright (c) 2015, 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 'dart:io';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/analysis_options/analysis_options_provider.dart';
import 'package:analyzer/src/generated/engine.dart';
import 'package:analyzer/src/lint/io.dart';
import 'package:analyzer/src/lint/registry.dart';
import 'package:analyzer/src/task/options.dart';
import 'package:analyzer/src/utilities/legacy.dart';
import 'package:linter/src/analyzer.dart';
import 'package:linter/src/ast.dart';
import 'package:linter/src/formatter.dart';
import 'package:linter/src/rules.dart';
import 'package:linter/src/rules/implementation_imports.dart';
import 'package:linter/src/rules/package_prefixed_library_names.dart';
import 'package:linter/src/test_utilities/annotation.dart';
import 'package:linter/src/test_utilities/test_resource_provider.dart';
import 'package:linter/src/utils.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'experiments_test.dart' as experiment_tests;
import 'test_constants.dart';
import 'util/annotation_matcher.dart';
import 'util/test_utils.dart';
void main() {
group('rule tests', () {
setUp(setUpSharedTestEnvironment);
defineRuleTests();
experiment_tests.main();
defineRuleUnitTests();
});
}
/// Rule tests
void defineRuleTests() {
group('rule', () {
group('dart', () {
// Rule tests run with default analysis options.
testRules(ruleTestDataDir);
// Rule tests run against specific configurations.
for (var entry in Directory(testConfigDir).listSync()) {
if (entry is! Directory) continue;
group('(config: ${p.basename(entry.path)})', () {
var analysisOptionsFile =
File(p.join(entry.path, 'analysis_options.yaml'));
var analysisOptions = analysisOptionsFile.readAsStringSync();
testRules(ruleTestDataDir, analysisOptions: analysisOptions);
});
}
});
group('pub', () {
for (var entry in Directory(p.join(ruleTestDataDir, 'pub')).listSync()) {
if (entry is Directory) {
for (var child in entry.listSync()) {
if (child is File && isPubspecFile(child)) {
var ruleName = p.basename(entry.path);
testRule(ruleName, child);
}
}
}
}
});
});
}
void defineRuleUnitTests() {
group('uris', () {
group('isPackage', () {
for (var uri in [
Uri.parse('package:foo/src/bar.dart'),
Uri.parse('package:foo/src/baz/bar.dart')
]) {
test(uri.toString(), () {
expect(isPackage(uri), isTrue);
});
}
for (var uri in [
Uri.parse('foo/bar.dart'),
Uri.parse('src/bar.dart'),
Uri.parse('dart:async')
]) {
test(uri.toString(), () {
expect(isPackage(uri), isFalse);
});
}
});
group('samePackage', () {
test('identity', () {
expect(
samePackage(Uri.parse('package:foo/src/bar.dart'),
Uri.parse('package:foo/src/bar.dart')),
isTrue);
});
test('foo/bar.dart', () {
expect(
samePackage(Uri.parse('package:foo/src/bar.dart'),
Uri.parse('package:foo/bar.dart')),
isTrue);
});
});
group('implementation', () {
for (var uri in [
Uri.parse('package:foo/src/bar.dart'),
Uri.parse('package:foo/src/baz/bar.dart')
]) {
test(uri.toString(), () {
expect(isImplementation(uri), isTrue);
});
}
for (var uri in [
Uri.parse('package:foo/bar.dart'),
Uri.parse('src/bar.dart')
]) {
test(uri.toString(), () {
expect(isImplementation(uri), isFalse);
});
}
});
});
group('names', () {
group('keywords', () {
var good = ['class', 'if', 'assert', 'catch', 'import'];
testEach(good, isKeyWord, isTrue);
var bad = ['_class', 'iff', 'assert_', 'Catch'];
testEach(bad, isKeyWord, isFalse);
});
group('identifiers', () {
var good = [
'foo',
'_if',
'_',
'f2',
'fooBar',
'foo_bar',
'\$foo',
'foo\$Bar',
'foo\$'
];
testEach(good, isValidDartIdentifier, isTrue);
var bad = ['if', '42', '3', '2f'];
testEach(bad, isValidDartIdentifier, isFalse);
});
group('library_name_prefixes', () {
bool isGoodPrefix(List<String> v) => matchesOrIsPrefixedBy(
v[3],
Analyzer.facade.createLibraryNamePrefix(
libraryPath: v[0], projectRoot: v[1], packageName: v[2]));
var good = [
['/u/b/c/lib/src/a.dart', '/u/b/c', 'acme', 'acme.src.a'],
['/u/b/c/lib/a.dart', '/u/b/c', 'acme', 'acme.a'],
['/u/b/c/test/a.dart', '/u/b/c', 'acme', 'acme.test.a'],
['/u/b/c/test/data/a.dart', '/u/b/c', 'acme', 'acme.test.data.a'],
['/u/b/c/lib/acme.dart', '/u/b/c', 'acme', 'acme']
];
testEach(good, isGoodPrefix, isTrue);
var bad = [
['/u/b/c/lib/src/a.dart', '/u/b/c', 'acme', 'acme.a'],
['/u/b/c/lib/a.dart', '/u/b/c', 'acme', 'wrk.acme.a'],
['/u/b/c/test/a.dart', '/u/b/c', 'acme', 'acme.a'],
['/u/b/c/test/data/a.dart', '/u/b/c', 'acme', 'acme.test.a']
];
testEach(bad, isGoodPrefix, isFalse);
});
});
}
/// Handy for debugging.
void defineSoloRuleTest(String ruleToTest) {
for (var entry in Directory(ruleTestDataDir).listSync()) {
if (entry is! File || !isDartFile(entry)) continue;
var ruleName = p.basenameWithoutExtension(entry.path);
if (ruleName == ruleToTest) {
testRule(ruleName, entry);
}
}
}
void testRule(String ruleName, File file,
{bool debug = true,
bool failOnErrors = true,
bool useMockSdk = true,
String? analysisOptions}) {
test(ruleName, () async {
if (!file.existsSync()) {
throw Exception('No rule found defined at: ${file.path}');
}
// Disable this check until migration is complete internally.
noSoundNullSafety = false;
try {
var errorInfos = await _getErrorInfos(ruleName, file,
useMockSdk: useMockSdk,
debug: debug,
analysisOptions: analysisOptions);
_validateExpectedLints(file, errorInfos,
debug: debug,
failOnErrors: failOnErrors,
analysisOptions: analysisOptions);
} finally {
noSoundNullSafety = true;
}
});
}
void testRules(String ruleDir, {String? analysisOptions}) {
for (var entry in Directory(ruleDir).listSync()) {
if (entry is! File || !isDartFile(entry)) continue;
var ruleName = p.basenameWithoutExtension(entry.path);
testRule(ruleName, entry, analysisOptions: analysisOptions);
}
}
Future<Iterable<AnalysisErrorInfo>> _getErrorInfos(String ruleName, File file,
{required bool useMockSdk,
required bool debug,
required String? analysisOptions}) async {
registerLintRules(inTestMode: debug);
var rule = Registry.ruleRegistry[ruleName];
if (rule == null) {
fail('rule `$ruleName` is not registered; unable to test.');
}
if (useMockSdk) {
var driver = buildDriver(rule, file, analysisOptions: analysisOptions);
return await driver.lintFiles([file]);
}
var path = p.normalize(file.absolute.path);
var collection = AnalysisContextCollection(
includedPaths: [path],
resourceProvider: PhysicalResourceProvider.INSTANCE,
);
var context = collection.contexts[0];
var options = context.analysisOptions as AnalysisOptionsImpl;
options.lintRules = context.analysisOptions.lintRules.toList();
options.lintRules.add(rule);
options.lint = true;
var result =
await context.currentSession.getResolvedUnit(path) as ResolvedUnitResult;
return [
AnalysisErrorInfoImpl(
result.errors,
result.lineInfo,
)
];
}
/// Parse lint annotations in the given [file] and validate that they correspond
/// with errors in the provided [errorInfos].
void _validateExpectedLints(File file, Iterable<AnalysisErrorInfo> errorInfos,
{bool debug = true, bool failOnErrors = true, String? analysisOptions}) {
var expected = <AnnotationMatcher>[];
var lineNumber = 1;
for (var line in file.readAsLinesSync()) {
var annotation = extractAnnotation(lineNumber, line);
if (annotation != null) {
expected.add(AnnotationMatcher(annotation));
}
++lineNumber;
}
var actual = <Annotation>[];
var errors = <String>[];
for (var info in errorInfos) {
for (var error in info.errors) {
var errorType = error.errorCode.type;
if (errorType == ErrorType.LINT) {
actual.add(Annotation.forError(error, info.lineInfo));
} else if (failOnErrors && errorType.severity == ErrorSeverity.ERROR) {
var location = info.lineInfo.getLocation(error.offset);
errors.add(
'${file.path} ${location.lineNumber}:${location.columnNumber} ${error.message}');
}
}
}
if (errors.isNotEmpty) {
fail(['Unexpected diagnostics:\n', ...errors].join('\n'));
}
actual.sort();
try {
expect(actual, unorderedMatches(expected));
// TODO (asashour): to be removed after fixing
// https://github.com/dart-lang/linter/issues/909
// ignore: avoid_catches_without_on_clauses
} catch (_) {
if (debug) {
// Dump results for debugging purposes.
// AST
var optionsProvider = AnalysisOptionsProvider();
var optionMap = optionsProvider.getOptionsFromString(analysisOptions);
var optionsImpl = AnalysisOptionsImpl();
applyToAnalysisOptions(optionsImpl, optionMap);
var features = optionsImpl.contextFeatures;
FileSpelunker(file.absolute.path, featureSet: features).spelunk();
printToConsole('');
// Lints.
ResultReporter(errorInfos).write();
}
// Rethrow and fail.
rethrow;
}
}
/// A [LintFilter] that filters no lint.
class NoFilter implements LintFilter {
@override
bool filter(AnalysisError lint) => false;
}
/// A [DetailedReporter] that filters no lint, only used in debug mode, when
/// actual lints do not match expectations.
class ResultReporter extends DetailedReporter {
ResultReporter(Iterable<AnalysisErrorInfo> errors)
: super(errors, NoFilter(), stdout);
}