| // Copyright (c) 2021, 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:convert' show json; |
| |
| import 'package:analyzer/dart/analysis/results.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/dart/analysis/analysis_context_collection.dart'; |
| import 'package:analyzer/src/dart/analysis/byte_store.dart'; |
| import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart'; |
| import 'package:analyzer/src/dart/analysis/experiments.dart'; |
| import 'package:analyzer/src/error/codes.dart'; |
| import 'package:analyzer/src/lint/pub.dart'; |
| import 'package:analyzer/src/lint/registry.dart'; |
| import 'package:analyzer/src/lint/util.dart'; |
| import 'package:analyzer/src/test_utilities/mock_sdk.dart'; |
| import 'package:analyzer/src/test_utilities/package_config_file_builder.dart'; |
| import 'package:analyzer/src/test_utilities/resource_provider_mixin.dart'; |
| import 'package:analyzer_utilities/test/experiments/experiments.dart'; |
| import 'package:analyzer_utilities/test/mock_packages/mock_packages.dart'; |
| import 'package:collection/collection.dart'; |
| import 'package:linter/src/analyzer.dart'; |
| import 'package:linter/src/rules.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:test/test.dart'; |
| |
| export 'package:analyzer/src/dart/error/syntactic_errors.dart'; |
| export 'package:analyzer/src/error/codes.dart'; |
| export 'package:analyzer/src/test_utilities/package_config_file_builder.dart'; |
| export 'package:linter/src/lint_names.dart'; |
| |
| // TODO(srawlins): This is duplicate with |
| // pkg/analyzer/test/src/dart/resolution/context_collection_resolution.dart and |
| // and pkg/analysis_server/test/analysis_server_base.dart. Keep them as |
| // consistent with each other as they are today. Ultimately combine them in a |
| // shared analyzer test utilities package. |
| String analysisOptionsContent({ |
| List<String> experiments = const [], |
| List<String> rules = const [], |
| }) { |
| var buffer = StringBuffer(); |
| |
| buffer.writeln('analyzer:'); |
| buffer.writeln(' enable-experiment:'); |
| for (var experiment in experiments) { |
| buffer.writeln(' - $experiment'); |
| } |
| |
| buffer.writeln(' optional-checks:'); |
| buffer.writeln(' propagate-linter-exceptions: true'); |
| |
| buffer.writeln('linter:'); |
| buffer.writeln(' rules:'); |
| for (var rule in rules) { |
| buffer.writeln(' - $rule'); |
| } |
| |
| return buffer.toString(); |
| } |
| |
| ExpectedDiagnostic error( |
| ErrorCode code, |
| int offset, |
| int length, { |
| Pattern? messageContains, |
| }) => _ExpectedError(code, offset, length, messageContains: messageContains); |
| |
| // TODO(srawlins): This is duplicate with |
| // pkg/analyzer/test/src/dart/resolution/context_collection_resolution.dart. |
| // Keep them as consistent with each other as they are today. Ultimately combine |
| // them in a shared analyzer test utilities package. |
| String pubspecYamlContent({String? name}) { |
| var buffer = StringBuffer(); |
| |
| if (name != null) { |
| buffer.writeln('name: $name'); |
| } |
| |
| return buffer.toString(); |
| } |
| |
| typedef DiagnosticMatcher = bool Function(AnalysisError error); |
| |
| /// A description of a diagnostic that is expected to be reported. |
| class ExpectedDiagnostic { |
| final DiagnosticMatcher _diagnosticMatcher; |
| |
| /// The offset of the beginning of the diagnostic's region. |
| final int _offset; |
| |
| /// The offset of the beginning of the diagnostic's region. |
| final int _length; |
| |
| /// A pattern that should be contained in the diagnostic message or `null` if |
| /// the message contents should not be checked. |
| final Pattern? _messageContains; |
| |
| // A pattern that should be contained in the error's correction message, or |
| // `null` if the correction message contents should not be checked. |
| final Pattern? _correctionContains; |
| |
| /// Initialize a newly created diagnostic description. |
| ExpectedDiagnostic( |
| this._diagnosticMatcher, |
| this._offset, |
| this._length, { |
| Pattern? messageContains, |
| Pattern? correctionContains, |
| }) : _messageContains = messageContains, |
| _correctionContains = correctionContains; |
| |
| /// Whether the [error] matches this description of what it's expected to be. |
| bool matches(AnalysisError error) { |
| if (!_diagnosticMatcher(error)) return false; |
| if (error.offset != _offset) return false; |
| if (error.length != _length) return false; |
| if (_messageContains != null && !error.message.contains(_messageContains)) { |
| return false; |
| } |
| if (_correctionContains != null) { |
| if (error.correction == null || |
| !error.correction!.contains(_correctionContains)) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| } |
| |
| mixin LanguageVersion219Mixin on PubPackageResolutionTest { |
| @override |
| String? get testPackageLanguageVersion => '2.19'; |
| } |
| |
| abstract class LintRuleTest extends PubPackageResolutionTest { |
| String get lintRule; |
| |
| @override |
| List<String> get _lintRules { |
| var ruleName = lintRule; |
| if (!Registry.ruleRegistry.any((r) => r.name == ruleName)) { |
| throw Exception("Unrecognized rule: '$ruleName'"); |
| } |
| return [ruleName]; |
| } |
| |
| ExpectedDiagnostic lint( |
| int offset, |
| int length, { |
| Pattern? messageContains, |
| Pattern? correctionContains, |
| String? name, |
| }) => _ExpectedLint( |
| name ?? lintRule, |
| offset, |
| length, |
| messageContains: messageContains, |
| correctionContains: correctionContains, |
| ); |
| } |
| |
| class PubPackageResolutionTest extends _ContextResolutionTest { |
| final List<String> _lintRules = const []; |
| |
| bool get addFixnumPackageDep => false; |
| |
| bool get addFlutterPackageDep => false; |
| |
| bool get addJsPackageDep => false; |
| |
| bool get addKernelPackageDep => false; |
| |
| bool get addMetaPackageDep => false; |
| |
| bool get addReflectiveTestLoaderPackageDep => false; |
| |
| bool get dumpAstOnFailures => true; |
| |
| List<String> get experiments => experimentsForTests; |
| |
| String get testFileName => 'test.dart'; |
| |
| @override |
| String get testFilePath => '$testPackageLibPath/$testFileName'; |
| |
| String? get testPackageLanguageVersion => null; |
| |
| String get testPackageLibPath => '$testPackageRootPath/lib'; |
| |
| String get testPackagePubspecPath => '$testPackageRootPath/pubspec.yaml'; |
| |
| String get testPackageRootPath => '$workspaceRootPath/test'; |
| |
| String get workspaceRootPath => '/home'; |
| |
| @override |
| List<String> get _collectionIncludedPaths => [workspaceRootPath]; |
| |
| /// Asserts that the number of diagnostics reported in [content] matches the |
| /// number of [expectedDiagnostics] and that they have the expected error |
| /// descriptions and locations. |
| /// |
| /// The order in which the diagnostics were gathered is ignored. |
| Future<void> assertDiagnostics( |
| String content, |
| List<ExpectedDiagnostic> expectedDiagnostics, |
| ) async { |
| addTestFile(content); |
| await resolveTestFile(); |
| await _assertDiagnosticsIn(_errors, expectedDiagnostics); |
| } |
| |
| /// Asserts that the number of diagnostics that have been gathered at [path] |
| /// matches the number of [expectedDiagnostics] and that they have the |
| /// expected error descriptions and locations. |
| /// |
| /// The order in which the diagnostics were gathered is ignored. |
| Future<void> assertDiagnosticsInFile( |
| String path, |
| List<ExpectedDiagnostic> expectedDiagnostics, |
| ) async { |
| await _resolveFile(path); |
| await _assertDiagnosticsIn(_errors, expectedDiagnostics); |
| } |
| |
| /// Asserts that the diagnostics for each `path` match those in the paired |
| /// `expectedDiagnostics`. |
| /// |
| /// The unit at each path needs to have already been written to the file |
| /// system before calling this method. |
| Future<void> assertDiagnosticsInUnits( |
| List<(String path, List<ExpectedDiagnostic> expectedDiagnostics)> |
| unitsAndDiagnostics, |
| ) async { |
| for (var (path, expectedDiagnostics) in unitsAndDiagnostics) { |
| result = await resolveFile(convertPath(path)); |
| await _assertDiagnosticsIn(result.errors, expectedDiagnostics); |
| } |
| } |
| |
| /// Asserts that there are no diagnostics in the given [content]. |
| Future<void> assertNoDiagnostics(String content) async => |
| assertDiagnostics(content, const []); |
| |
| /// Asserts that there are no diagnostics in the file at the given [path]. |
| Future<void> assertNoDiagnosticsInFile(String path) async => |
| assertDiagnosticsInFile(path, const []); |
| |
| /// Asserts that no diagnostics are reported when resolving [content]. |
| Future<void> assertNoPubspecDiagnostics(String content) async { |
| newFile(testPackagePubspecPath, content); |
| var errors = await _resolvePubspecFile(content); |
| await _assertDiagnosticsIn(errors, []); |
| } |
| |
| /// Asserts that [expectedDiagnostics] are reported when resolving [content]. |
| Future<void> assertPubspecDiagnostics( |
| String content, |
| List<ExpectedDiagnostic> expectedDiagnostics, |
| ) async { |
| newFile(testPackagePubspecPath, content); |
| var errors = await _resolvePubspecFile(content); |
| await _assertDiagnosticsIn(errors, expectedDiagnostics); |
| } |
| |
| @override |
| @mustCallSuper |
| void setUp() { |
| super.setUp(); |
| // Check for any needlessly enabled experiments. |
| for (var experiment in experiments) { |
| var feature = ExperimentStatus.knownFeatures[experiment]; |
| if (feature?.isEnabledByDefault ?? false) { |
| fail( |
| "The '$experiment' experiment is enabled by default, " |
| 'try removing it from `experiments`.', |
| ); |
| } |
| } |
| |
| newAnalysisOptionsYamlFile( |
| testPackageRootPath, |
| analysisOptionsContent(experiments: experiments, rules: _lintRules), |
| ); |
| writeTestPackageConfig(PackageConfigFileBuilder()); |
| _writeTestPackagePubspecYamlFile(pubspecYamlContent(name: 'test')); |
| } |
| |
| void writePackageConfig(String path, PackageConfigFileBuilder config) { |
| newFile(path, config.toContent(toUriStr: toUriStr)); |
| } |
| |
| void writeTestPackageConfig(PackageConfigFileBuilder config) { |
| var configCopy = config.copy(); |
| |
| configCopy.add( |
| name: 'test', |
| rootPath: testPackageRootPath, |
| languageVersion: testPackageLanguageVersion, |
| ); |
| |
| if (addFixnumPackageDep) { |
| var fixnumPath = addFixnum().parent.path; |
| configCopy.add(name: 'fixnum', rootPath: fixnumPath); |
| } |
| |
| if (addFlutterPackageDep) { |
| var uiPath = addUI().parent.path; |
| configCopy.add(name: 'ui', rootPath: uiPath); |
| |
| var flutterPath = addFlutter().parent.path; |
| configCopy.add(name: 'flutter', rootPath: flutterPath); |
| } |
| |
| if (addJsPackageDep) { |
| var jsPath = addJs().parent.path; |
| configCopy.add(name: 'js', rootPath: jsPath); |
| } |
| |
| if (addKernelPackageDep) { |
| var kernelPath = addKernel().parent.path; |
| configCopy.add(name: 'kernel', rootPath: kernelPath); |
| } |
| |
| if (addMetaPackageDep) { |
| var metaPath = addMeta().parent.path; |
| configCopy.add(name: 'meta', rootPath: metaPath); |
| } |
| |
| if (addReflectiveTestLoaderPackageDep) { |
| var testReflectiveLoaderPath = |
| '$workspaceRootPath/test_reflective_loader'; |
| newFile('$testReflectiveLoaderPath/lib/test_reflective_loader.dart', r''' |
| library test_reflective_loader; |
| |
| const Object reflectiveTest = _ReflectiveTest(); |
| class _ReflectiveTest { |
| const _ReflectiveTest(); |
| } |
| '''); |
| configCopy.add( |
| name: 'test_reflective_loader', |
| rootPath: testReflectiveLoaderPath, |
| ); |
| } |
| |
| var path = '$testPackageRootPath/.dart_tool/package_config.json'; |
| writePackageConfig(path, configCopy); |
| } |
| |
| /// Asserts that the diagnostics in [errors] match [expectedDiagnostics]. |
| Future<void> _assertDiagnosticsIn( |
| List<AnalysisError> errors, |
| List<ExpectedDiagnostic> expectedDiagnostics, |
| ) async { |
| // |
| // Match actual diagnostics to expected diagnostics. |
| // |
| var unmatchedActual = errors.toList(); |
| var unmatchedExpected = expectedDiagnostics.toList(); |
| var actualIndex = 0; |
| while (actualIndex < unmatchedActual.length) { |
| var matchFound = false; |
| var expectedIndex = 0; |
| while (expectedIndex < unmatchedExpected.length) { |
| if (unmatchedExpected[expectedIndex].matches( |
| unmatchedActual[actualIndex], |
| )) { |
| matchFound = true; |
| unmatchedActual.removeAt(actualIndex); |
| unmatchedExpected.removeAt(expectedIndex); |
| break; |
| } |
| expectedIndex++; |
| } |
| if (!matchFound) { |
| actualIndex++; |
| } |
| } |
| // |
| // Write the results. |
| // |
| var buffer = StringBuffer(); |
| if (unmatchedExpected.isNotEmpty) { |
| buffer.writeln('Expected but did not find:'); |
| for (var expected in unmatchedExpected) { |
| buffer.write(' '); |
| if (expected is _ExpectedError) { |
| buffer.write(expected._code); |
| } |
| if (expected is _ExpectedLint) { |
| buffer.write(expected._lintName); |
| } |
| buffer.write(' ['); |
| buffer.write(expected._offset); |
| buffer.write(', '); |
| buffer.write(expected._length); |
| if (expected._messageContains != null) { |
| buffer.write(', messageContains: '); |
| buffer.write(json.encode(expected._messageContains.toString())); |
| } |
| if (expected._correctionContains != null) { |
| buffer.write(', correctionContains: '); |
| buffer.write(json.encode(expected._correctionContains.toString())); |
| } |
| buffer.writeln(']'); |
| } |
| } |
| if (unmatchedActual.isNotEmpty) { |
| if (buffer.isNotEmpty) { |
| buffer.writeln(); |
| } |
| buffer.writeln('Found but did not expect:'); |
| for (var actual in unmatchedActual) { |
| buffer.write(' '); |
| buffer.write(actual.errorCode); |
| buffer.write(' ['); |
| buffer.write(actual.offset); |
| buffer.write(', '); |
| buffer.write(actual.length); |
| buffer.write(', '); |
| buffer.write(actual.message); |
| if (actual.correctionMessage != null) { |
| buffer.write(', '); |
| buffer.write(json.encode(actual.correctionMessage)); |
| } |
| buffer.writeln(']'); |
| } |
| } |
| if (buffer.isNotEmpty) { |
| errors.sort((first, second) => first.offset.compareTo(second.offset)); |
| buffer.writeln(); |
| buffer.writeln('To accept the current state, expect:'); |
| for (var actual in errors) { |
| late String diagnosticKind; |
| Object? description; |
| if (actual.errorCode is LintCode) { |
| diagnosticKind = 'lint'; |
| } else { |
| diagnosticKind = 'error'; |
| description = actual.errorCode; |
| } |
| buffer.write(' $diagnosticKind('); |
| if (description != null) { |
| buffer.write(description); |
| buffer.write(', '); |
| } |
| buffer.write(actual.offset); |
| buffer.write(', '); |
| buffer.write(actual.length); |
| buffer.writeln('),'); |
| } |
| |
| if (dumpAstOnFailures) { |
| buffer.writeln(); |
| buffer.writeln(); |
| try { |
| var astSink = StringBuffer(); |
| |
| StringSpelunker( |
| result.unit.toSource(), |
| sink: astSink, |
| featureSet: result.unit.featureSet, |
| ).spelunk(); |
| buffer.write(astSink); |
| buffer.writeln(); |
| // I hereby choose to catch this type. |
| // ignore: avoid_catching_errors |
| } on ArgumentError catch (_) { |
| // Perhaps we encountered a parsing error while spelunking. |
| } |
| } |
| |
| fail(buffer.toString()); |
| } |
| } |
| |
| Future<List<AnalysisError>> _resolvePubspecFile(String content) async { |
| var path = convertPath(testPackagePubspecPath); |
| var pubspecRules = <LintRule, PubspecVisitor<Object?>>{}; |
| for (var rule in Registry.ruleRegistry.where( |
| (rule) => _lintRules.contains(rule.name), |
| )) { |
| var visitor = rule.getPubspecVisitor(); |
| 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 = RecordingErrorListener(); |
| var file = resourceProvider.getFile(path); |
| var reporter = ErrorReporter(listener, FileSource(file, sourceUri)); |
| for (var entry in pubspecRules.entries) { |
| entry.key.reporter = reporter; |
| pubspecAst.accept(entry.value); |
| } |
| return [...listener.errors]; |
| } |
| |
| void _writeTestPackagePubspecYamlFile(String content) { |
| newPubspecYamlFile(testPackageRootPath, content); |
| } |
| } |
| |
| abstract class _ContextResolutionTest |
| with MockPackagesMixin, ResourceProviderMixin { |
| static bool _lintRulesAreRegistered = false; |
| |
| /// The byte store that is reused between tests. This allows reusing all |
| /// unlinked and linked summaries for SDK, so that tests run much faster. |
| /// However nothing is preserved between Dart VM runs, so changes to the |
| /// implementation are still fully verified. |
| static final MemoryByteStore _sharedByteStore = MemoryByteStore(); |
| |
| final MemoryByteStore _byteStore = _sharedByteStore; |
| |
| AnalysisContextCollectionImpl? _analysisContextCollection; |
| |
| late ResolvedUnitResult result; |
| |
| /// Error codes that by default should be ignored in test expectations. |
| List<ErrorCode> get ignoredErrorCodes => [WarningCode.UNUSED_LOCAL_VARIABLE]; |
| |
| /// The path to the root of the external packages. |
| @override |
| String get packagesRootPath => '/packages'; |
| |
| String get testFilePath; |
| |
| List<String> get _collectionIncludedPaths; |
| |
| /// The analysis errors that were computed during analysis. |
| List<AnalysisError> get _errors => |
| result.errors |
| .whereNot((e) => ignoredErrorCodes.any((c) => e.errorCode == c)) |
| .toList(); |
| |
| Folder get _sdkRoot => newFolder('/sdk'); |
| |
| void addTestFile(String content) { |
| newFile(testFilePath, content); |
| } |
| |
| @override |
| File newFile(String path, String content) { |
| if (_analysisContextCollection != null && !path.endsWith('.dart')) { |
| throw StateError('Only dart files can be changed after analysis.'); |
| } |
| |
| return super.newFile(path, content); |
| } |
| |
| /// Resolves a Dart source file at [path]. |
| /// |
| /// [path] must be converted for this file system. |
| Future<ResolvedUnitResult> resolveFile(String path) async { |
| var analysisContext = _contextFor(path); |
| var session = analysisContext.currentSession; |
| return await session.getResolvedUnit(path) as ResolvedUnitResult; |
| } |
| |
| Future<void> resolveTestFile() => _resolveFile(testFilePath); |
| |
| @mustCallSuper |
| void setUp() { |
| if (!_lintRulesAreRegistered) { |
| registerLintRules(); |
| _lintRulesAreRegistered = true; |
| } |
| |
| createMockSdk(resourceProvider: resourceProvider, root: _sdkRoot); |
| } |
| |
| @mustCallSuper |
| Future<void> tearDown() async { |
| await _analysisContextCollection?.dispose(); |
| _analysisContextCollection = null; |
| } |
| |
| DriverBasedAnalysisContext _contextFor(String path) { |
| _createAnalysisContexts(); |
| |
| var convertedPath = convertPath(path); |
| return _analysisContextCollection!.contextFor(convertedPath); |
| } |
| |
| /// Creates all analysis contexts in [_collectionIncludedPaths]. |
| void _createAnalysisContexts() { |
| if (_analysisContextCollection != null) { |
| return; |
| } |
| |
| _analysisContextCollection = AnalysisContextCollectionImpl( |
| byteStore: _byteStore, |
| declaredVariables: {}, |
| enableIndex: true, |
| includedPaths: _collectionIncludedPaths.map(convertPath).toList(), |
| resourceProvider: resourceProvider, |
| sdkPath: _sdkRoot.path, |
| ); |
| } |
| |
| /// Resolves the file with the [path] into [result]. |
| Future<void> _resolveFile(String path) async { |
| var convertedPath = convertPath(path); |
| |
| result = await resolveFile(convertedPath); |
| } |
| } |
| |
| /// A description of an expected error. |
| final class _ExpectedError extends ExpectedDiagnostic { |
| final ErrorCode _code; |
| |
| _ExpectedError(this._code, int offset, int length, {Pattern? messageContains}) |
| : super( |
| (error) => error.errorCode == _code, |
| offset, |
| length, |
| messageContains: messageContains, |
| ); |
| } |
| |
| /// A description of an expected lint rule violation. |
| final class _ExpectedLint extends ExpectedDiagnostic { |
| final String _lintName; |
| |
| _ExpectedLint( |
| this._lintName, |
| int offset, |
| int length, { |
| super.messageContains, |
| super.correctionContains, |
| }) : super((error) => error.errorCode.name == _lintName, offset, length); |
| } |