| // 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 'package:analysis_server/plugin/edit/fix/fix_core.dart'; |
| import 'package:analysis_server/src/protocol_server.dart' show SourceEdit; |
| import 'package:analysis_server/src/services/correction/change_workspace.dart'; |
| import 'package:analysis_server/src/services/correction/fix.dart'; |
| import 'package:analysis_server/src/services/correction/fix_internal.dart'; |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:analyzer/error/error.dart'; |
| import 'package:analyzer/file_system/file_system.dart'; |
| import 'package:analyzer/file_system/overlay_file_system.dart'; |
| import 'package:analyzer/instrumentation/service.dart'; |
| import 'package:analyzer/src/dart/analysis/analysis_context_collection.dart'; |
| import 'package:analyzer/src/dart/error/lint_codes.dart'; |
| import 'package:collection/collection.dart'; |
| |
| /// The root of a set of classes that support testing for lint fixes. |
| /// |
| /// These classes work as a sequence: |
| /// 1. Create this tested with necessary SDK and package config. |
| /// 2. Configure any overlays on top of the file system with [updateFile]. |
| /// 3. Request fixes for a file using [fixesForSingleLint]. |
| /// 4. Use the [LintFixTesterWithFixes] to check how many fixes there are. |
| /// 5. If there is a single fix, use [LintFixTesterWithSingleFix] to verify |
| /// the fixed content for specific files. |
| class LintFixTester { |
| final OverlayResourceProvider _resourceProvider; |
| final String sdkPath; |
| final String? packageConfigPath; |
| |
| /// If `false`, then we have already computed lints for this tester, |
| /// and updating the file system (as much as we can observe it) should |
| /// not be allowed. |
| bool _canUpdateResourceProvider = true; |
| |
| LintFixTester({ |
| required ResourceProvider resourceProvider, |
| required this.sdkPath, |
| required this.packageConfigPath, |
| }) : _resourceProvider = OverlayResourceProvider(resourceProvider); |
| |
| /// Prepare fixes for a single lint in the file with the [path] |
| /// |
| /// If [inFile] is `false`, there must be exactly one diagnostic in the file, |
| /// and it is a lint. |
| /// |
| /// If [inFile] is `true`, there must be one or more diagnostics, but all |
| /// of them must have the same error code, and it must be a lint. |
| /// |
| /// Throws [StateError] if an expectation is not satisfied. |
| Future<LintFixTesterWithFixes> fixesForSingleLint({ |
| required String path, |
| required bool inFile, |
| }) async { |
| _canUpdateResourceProvider = true; |
| |
| var collection = AnalysisContextCollectionImpl( |
| includedPaths: [path], |
| resourceProvider: _resourceProvider, |
| sdkPath: sdkPath, |
| packagesFile: packageConfigPath, |
| ); |
| var analysisContext = collection.contextFor(path); |
| var analysisSession = analysisContext.currentSession; |
| |
| var unitResult = await analysisSession.getResolvedUnit(path); |
| unitResult as ResolvedUnitResult; |
| |
| AnalysisError error; |
| var errors = unitResult.errors; |
| if (inFile) { |
| var groups = errors.groupListsBy((error) => error.errorCode); |
| if (groups.length != 1) { |
| throw StateError( |
| 'Exactly one error code expected:' |
| '\n$errors\n${groups.keys.toList()}', |
| ); |
| } |
| error = errors.first; |
| } else { |
| if (errors.length != 1) { |
| throw StateError('Exactly one lint expected: $errors'); |
| } |
| error = errors.single; |
| } |
| |
| if (error.errorCode is! LintCode) { |
| throw StateError('A lint expected: $errors'); |
| } |
| |
| var workspace = DartChangeWorkspace([analysisSession]); |
| var context = DartFixContextImpl( |
| InstrumentationService.NULL_SERVICE, |
| workspace, |
| unitResult, |
| error, |
| (name) => const [], |
| ); |
| |
| List<Fix> fixes; |
| if (inFile) { |
| var fixInFileProcessor = FixInFileProcessor(context); |
| fixes = await fixInFileProcessor.compute(); |
| } else { |
| fixes = await FixProcessor(context).compute(); |
| fixes.removeWhere( |
| (fix) => |
| fix.kind == DartFixKind.IGNORE_ERROR_LINE || |
| fix.kind == DartFixKind.IGNORE_ERROR_FILE, |
| ); |
| } |
| |
| return LintFixTesterWithFixes(parent: this, fixes: fixes); |
| } |
| |
| /// Update the view on the file system so that the final with the [path] |
| /// is considered to have the given [content]. The actual file system is |
| /// not changed. |
| /// |
| /// This method should not be used after any analysis is performed, such |
| /// as invocation of [fixesForSingleLint], will throw [StateError]. |
| void updateFile({ |
| required String path, |
| required String content, |
| }) { |
| if (!_canUpdateResourceProvider) { |
| throw StateError('Diagnostics were already computed.'); |
| } |
| |
| _resourceProvider.setOverlay( |
| path, |
| content: content, |
| modificationStamp: 0, |
| ); |
| } |
| } |
| |
| class LintFixTesterWithFixes { |
| final LintFixTester _parent; |
| final List<Fix> fixes; |
| |
| LintFixTesterWithFixes({ |
| required LintFixTester parent, |
| required this.fixes, |
| }) : _parent = parent; |
| |
| void assertNoFixes() { |
| if (fixes.isNotEmpty) { |
| throw StateError('Must have exactly zero fixes: $fixes'); |
| } |
| } |
| |
| LintFixTesterWithSingleFix assertSingleFix() { |
| if (fixes.length != 1) { |
| throw StateError('Must have exactly one fix: $fixes'); |
| } |
| |
| return LintFixTesterWithSingleFix( |
| parent: this, |
| fix: fixes.single, |
| ); |
| } |
| } |
| |
| class LintFixTesterWithSingleFix { |
| final LintFixTesterWithFixes _parent; |
| final Fix fix; |
| |
| LintFixTesterWithSingleFix({ |
| required LintFixTesterWithFixes parent, |
| required this.fix, |
| }) : _parent = parent; |
| |
| void assertFixedContentOfFile({ |
| required String path, |
| required String fixedContent, |
| }) { |
| var fileEdits = fix.change.edits; |
| var fileEdit = fileEdits.singleWhere( |
| (fileEdit) => fileEdit.file == path, |
| ); |
| |
| var resourceProvider = _parent._parent._resourceProvider; |
| var file = resourceProvider.getFile(path); |
| var fileContent = file.readAsStringSync(); |
| |
| var actualFixedContent = SourceEdit.applySequence( |
| fileContent, |
| fileEdit.edits, |
| ); |
| if (actualFixedContent != fixedContent) { |
| throw StateError('Not expected content:\n$actualFixedContent'); |
| } |
| } |
| |
| void assertNoFileEdit({required String path}) { |
| var fileEdits = fix.change.edits; |
| var filtered = fileEdits.where( |
| (fileEdit) => fileEdit.file == path, |
| ); |
| if (filtered.isNotEmpty) { |
| throw StateError('Expected no edit for $path: $fix'); |
| } |
| } |
| } |