diff --git a/packages/custom_lint/CHANGELOG.md b/packages/custom_lint/CHANGELOG.md index ef5db835..87e8528c 100644 --- a/packages/custom_lint/CHANGELOG.md +++ b/packages/custom_lint/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased 0.7.0 + +- `custom_lint --fix` and the generated "Fix all " assists + now correctly handle imports. +- Now supports a broad number of analyzer version. + ## 0.6.10 - 2024-10-10 - Support installing custom_lint plugins in `dependencies:` instead of `dev_dependencies` (thanks to @dickermoshe). diff --git a/packages/custom_lint/example/example_lint/lib/custom_lint_example_lint.dart b/packages/custom_lint/example/example_lint/lib/custom_lint_example_lint.dart index d23aa0a4..6647f974 100644 --- a/packages/custom_lint/example/example_lint/lib/custom_lint_example_lint.dart +++ b/packages/custom_lint/example/example_lint/lib/custom_lint_example_lint.dart @@ -1,4 +1,7 @@ -import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, necessary to support lower analyzer versions + LintCode; import 'package:analyzer/error/listener.dart'; import 'package:analyzer/source/source_range.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; diff --git a/packages/custom_lint/example/example_lint/pubspec.yaml b/packages/custom_lint/example/example_lint/pubspec.yaml index a9964685..d1ce269a 100644 --- a/packages/custom_lint/example/example_lint/pubspec.yaml +++ b/packages/custom_lint/example/example_lint/pubspec.yaml @@ -12,9 +12,3 @@ dependencies: dev_dependencies: custom_lint: - -dependency_overrides: - custom_lint: - path: ../../../custom_lint - custom_lint_core: - path: ../../../custom_lint_core \ No newline at end of file diff --git a/packages/custom_lint/example/pubspec.yaml b/packages/custom_lint/example/pubspec.yaml index 128c97e3..47fac126 100644 --- a/packages/custom_lint/example/pubspec.yaml +++ b/packages/custom_lint/example/pubspec.yaml @@ -11,11 +11,3 @@ dev_dependencies: custom_lint: custom_lint_example_lint: path: ./example_lint - -dependency_overrides: - custom_lint: - path: ../../custom_lint - custom_lint_builder: - path: ../../custom_lint_builder - custom_lint_core: - path: ../../custom_lint_core diff --git a/packages/custom_lint/test/cli_process_test.dart b/packages/custom_lint/test/cli_process_test.dart index 8f8e9514..92bddc09 100644 --- a/packages/custom_lint/test/cli_process_test.dart +++ b/packages/custom_lint/test/cli_process_test.dart @@ -588,6 +588,61 @@ Analyzing... expect(process.exitCode, 1); }); + test('Supports adding imports', () async { + final fixedPlugin = createPluginSource([ + TestLintRule( + code: 'oy', + message: 'Oy', + onVariable: 'if (node.name.toString().endsWith("fixed")) return;', + fixes: [ + TestLintFix( + name: 'OyFix', + dartBuilderCode: r''' +builder.importLibrary(Uri.parse('package:path/path.dart')); + +builder.addSimpleReplacement(node.name.sourceRange, '${node.name}fixed'); +''', + ), + ], + ), + ]); + + final plugin = createPlugin(name: 'test_lint', main: fixedPlugin); + + final app = createLintUsage( + name: 'test_app', + source: { + 'lib/main.dart': ''' +void fn() {} +void fn2() {} +''', + }, + plugins: {'test_lint': plugin.uri}, + ); + + final process = await Process.run( + 'dart', + [customLintBinPath, '--fix'], + workingDirectory: app.path, + ); + + expect(trimDependencyOverridesWarning(process.stderr), isEmpty); + + expect(app.file('lib', 'main.dart').readAsStringSync(), ''' +import 'package:path/path.dart'; + +void fnfixed() {} +void fn2fixed() {} +'''); + + expect(process.stdout, ''' +Analyzing... + +No issues found! +'''); + expect(process.exitCode, 0); + }); + test('Can fix all lints', () async { final plugin = createPlugin(name: 'test_lint', main: fixedPlugin); diff --git a/packages/custom_lint/test/cli_test.dart b/packages/custom_lint/test/cli_test.dart index f5550895..68be191b 100644 --- a/packages/custom_lint/test/cli_test.dart +++ b/packages/custom_lint/test/cli_test.dart @@ -1,7 +1,10 @@ import 'dart:convert'; import 'dart:io'; -import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; import 'package:custom_lint/src/output/output_format.dart'; import 'package:test/test.dart'; diff --git a/packages/custom_lint/test/create_project.dart b/packages/custom_lint/test/create_project.dart index b0e50fe1..3d1560d5 100644 --- a/packages/custom_lint/test/create_project.dart +++ b/packages/custom_lint/test/create_project.dart @@ -1,7 +1,10 @@ import 'dart:convert'; import 'dart:io'; -import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; import 'package:path/path.dart'; import 'package:test/scaffolding.dart'; @@ -100,10 +103,13 @@ class TestLintFix { TestLintFix({ required this.name, this.nodeVisitor, + this.dartBuilderCode = + r"builder.addSimpleReplacement(node.name.sourceRange, '${node.name}fixed');", }); final String name; final String? nodeVisitor; + final String? dartBuilderCode; void write(StringBuffer buffer, TestLintRule rule) { buffer.write(''' @@ -124,9 +130,9 @@ class $name extends DartFix { message: 'Fix ${rule.code}', ); - ${nodeVisitor ?? r''' + ${nodeVisitor ?? ''' changeBuilder.addDartFileEdit((builder) { - builder.addSimpleReplacement(node.name.sourceRange, '${node.name}fixed'); + $dartBuilderCode }); '''} }); @@ -251,6 +257,14 @@ dev_dependencies: custom_lint: path: ${PeerProjectMeta.current.customLintPath} ${installAsDevDependency ? pluginDependencies : ""} + +dependency_overrides: + custom_lint: + path: ${PeerProjectMeta.current.customLintPath} + custom_lint_core: + path: ${PeerProjectMeta.current.customLintCorePath} + custom_lint_builder: + path: ${PeerProjectMeta.current.customLintBuilderPath} ''', packageConfig: createPackageConfig( plugins: {...plugins, ...extraPackageConfig}, diff --git a/packages/custom_lint/test/fixes_test.dart b/packages/custom_lint/test/fixes_test.dart index 686952a8..ba8567c5 100644 --- a/packages/custom_lint/test/fixes_test.dart +++ b/packages/custom_lint/test/fixes_test.dart @@ -58,6 +58,8 @@ final multiChangeFixPlugin = createPluginSource([ ), ]); +const ignoreId = '<>'; + void main() { test('Can emit fixes', () async { final plugin = createPlugin( @@ -89,11 +91,7 @@ void fn2() {} [await fixes, await fixes2] .expand((e) => e.fixes) .expand((e) => e.fixes) - .where( - (e) => - e.change.id != 'ignore_for_file' && - e.change.id != 'ignore_for_line', - ), + .where((e) => e.change.id != ignoreId), sources: ({'**/*': mainSource}, relativePath: app.path), file: Directory.current.file( 'test', @@ -125,11 +123,7 @@ void fn() {} final fixes = await runner.getFixes(mainPath, 6); expectMatchesGoldenFixes( - fixes.fixes.expand((e) => e.fixes).where( - (e) => - e.change.id != 'ignore_for_file' && - e.change.id != 'ignore_for_line', - ), + fixes.fixes.expand((e) => e.fixes).where((e) => e.change.id != ignoreId), sources: ({'**/*': mainSource}, relativePath: app.path), file: Directory.current.file( 'test', @@ -169,11 +163,7 @@ void fn4() {} final fixes = await runner.getFixes(mainPath, 6); expectMatchesGoldenFixes( - fixes.fixes.expand((e) => e.fixes).where( - (e) => - e.change.id != 'ignore_for_file' && - e.change.id != 'ignore_for_line', - ), + fixes.fixes.expand((e) => e.fixes).where((e) => e.change.id != ignoreId), sources: ({'**/*': mainSource}, relativePath: app.path), file: Directory.current.file( 'test', @@ -211,11 +201,7 @@ void fn2() {} [await fixes, await fixes2] .expand((e) => e.fixes) .expand((e) => e.fixes) - .where( - (e) => - e.change.id != 'ignore_for_file' && - e.change.id != 'ignore_for_line', - ), + .where((e) => e.change.id != ignoreId), sources: ({'**/*': mainSource}, relativePath: app.path), file: Directory.current.file( 'test', @@ -253,11 +239,7 @@ void fn2() {} [await fixes, await fixes2] .expand((e) => e.fixes) .expand((e) => e.fixes) - .where( - (e) => - e.change.id != 'ignore_for_file' && - e.change.id != 'ignore_for_line', - ), + .where((e) => e.change.id != ignoreId), sources: ({'**/*': mainSource}, relativePath: app.path), file: Directory.current.file( 'test', diff --git a/packages/custom_lint/test/goldens.dart b/packages/custom_lint/test/goldens.dart index ba9e0038..cf2d026d 100644 --- a/packages/custom_lint/test/goldens.dart +++ b/packages/custom_lint/test/goldens.dart @@ -55,9 +55,6 @@ String _encodePrioritizedSourceChanges( for (final prioritizedSourceChange in changes) { buffer.writeln('Message: `${prioritizedSourceChange.change.message}`'); buffer.writeln('Priority: ${prioritizedSourceChange.priority}'); - if (prioritizedSourceChange.change.id != null) { - buffer.writeln('Id: `${prioritizedSourceChange.change.id}`'); - } if (prioritizedSourceChange.change.selection case final selection?) { buffer.writeln( 'Selection: offset ${selection.offset} ; ' diff --git a/packages/custom_lint/test/goldens/fixes/add_ignore.diff b/packages/custom_lint/test/goldens/fixes/add_ignore.diff index 4603ca61..b1f675ea 100644 --- a/packages/custom_lint/test/goldens/fixes/add_ignore.diff +++ b/packages/custom_lint/test/goldens/fixes/add_ignore.diff @@ -1,6 +1,5 @@ Message: `Ignore "hello_world" for line` Priority: 1 -Id: `ignore_for_line` Diff for file `lib/main.dart:1`: ``` - void fn() {} @@ -12,7 +11,6 @@ void fn2() {} --- Message: `Ignore "hello_world" for file` Priority: 0 -Id: `ignore_for_file` Diff for file `lib/main.dart:1`: ``` - void fn() {} @@ -24,7 +22,6 @@ void fn2() {} --- Message: `Ignore "hello_world" for line` Priority: 1 -Id: `ignore_for_line` Diff for file `lib/main.dart:3`: ``` void fn() {} @@ -38,7 +35,6 @@ void fn() {} --- Message: `Ignore "hello_world" for file` Priority: 0 -Id: `ignore_for_file` Diff for file `lib/main.dart:1`: ``` - void fn() {} @@ -50,7 +46,6 @@ void fn2() {} --- Message: `Ignore "hello_world" for line` Priority: 1 -Id: `ignore_for_line` Diff for file `lib/main.dart:5`: ``` void fn2() {} @@ -62,7 +57,6 @@ void fn2() {} --- Message: `Ignore "hello_world" for file` Priority: 0 -Id: `ignore_for_file` Diff for file `lib/main.dart:1`: ``` - void fn() {} diff --git a/packages/custom_lint/test/goldens/fixes/update_ignore.diff b/packages/custom_lint/test/goldens/fixes/update_ignore.diff index c037b634..9805e57d 100644 --- a/packages/custom_lint/test/goldens/fixes/update_ignore.diff +++ b/packages/custom_lint/test/goldens/fixes/update_ignore.diff @@ -1,6 +1,5 @@ Message: `Ignore "hello_world" for line` Priority: 1 -Id: `ignore_for_line` Diff for file `lib/main.dart:5`: ``` @@ -12,7 +11,6 @@ void fn2() {} --- Message: `Ignore "hello_world" for file` Priority: 0 -Id: `ignore_for_file` Diff for file `lib/main.dart:4`: ``` void fn() {} diff --git a/packages/custom_lint/test/goldens/ignore_quick_fix.json b/packages/custom_lint/test/goldens/ignore_quick_fix.json index 9d7ed37e..16c0c663 100644 --- a/packages/custom_lint/test/goldens/ignore_quick_fix.json +++ b/packages/custom_lint/test/goldens/ignore_quick_fix.json @@ -16,7 +16,7 @@ } ], "linkedEditGroups": [], - "id": "ignore_for_line" + "id": "<>" } }, { @@ -36,7 +36,7 @@ } ], "linkedEditGroups": [], - "id": "ignore_for_file" + "id": "<>" } }, { @@ -56,7 +56,7 @@ } ], "linkedEditGroups": [], - "id": "ignore_for_line" + "id": "<>" } }, { @@ -76,7 +76,7 @@ } ], "linkedEditGroups": [], - "id": "ignore_for_file" + "id": "<>" } } ] \ No newline at end of file diff --git a/packages/custom_lint_builder/CHANGELOG.md b/packages/custom_lint_builder/CHANGELOG.md index c361d290..5016fd4a 100644 --- a/packages/custom_lint_builder/CHANGELOG.md +++ b/packages/custom_lint_builder/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased 0.7.0 + +- `custom_lint --fix` and the generated "Fix all " assists + now correctly handle imports. +- Now supports a broad number of analyzer version. + ## 0.6.10 - 2024-10-10 - `custom_lint` upgraded to `0.6.10` diff --git a/packages/custom_lint_builder/example/example_lint/lib/custom_lint_builder_example_lint.dart b/packages/custom_lint_builder/example/example_lint/lib/custom_lint_builder_example_lint.dart index 484d93bb..6e47ae81 100644 --- a/packages/custom_lint_builder/example/example_lint/lib/custom_lint_builder_example_lint.dart +++ b/packages/custom_lint_builder/example/example_lint/lib/custom_lint_builder_example_lint.dart @@ -1,4 +1,7 @@ -import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; import 'package:analyzer/error/listener.dart'; import 'package:analyzer/source/source_range.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; diff --git a/packages/custom_lint_builder/example/example_lint/pubspec.yaml b/packages/custom_lint_builder/example/example_lint/pubspec.yaml index 8f457d1c..cca04819 100644 --- a/packages/custom_lint_builder/example/example_lint/pubspec.yaml +++ b/packages/custom_lint_builder/example/example_lint/pubspec.yaml @@ -9,9 +9,3 @@ dependencies: analyzer_plugin: ^0.11.2 custom_lint_builder: path: ../../../custom_lint_builder - -dependency_overrides: - custom_lint: - path: ../../../custom_lint - custom_lint_core: - path: ../../../custom_lint_core \ No newline at end of file diff --git a/packages/custom_lint_builder/example/pubspec.yaml b/packages/custom_lint_builder/example/pubspec.yaml index c8ee79e7..2113819e 100644 --- a/packages/custom_lint_builder/example/pubspec.yaml +++ b/packages/custom_lint_builder/example/pubspec.yaml @@ -11,11 +11,3 @@ dev_dependencies: custom_lint: custom_lint_builder_example_lint: path: ./example_lint - -dependency_overrides: - custom_lint: - path: ../../custom_lint - custom_lint_builder: - path: ../../custom_lint_builder - custom_lint_core: - path: ../../custom_lint_core diff --git a/packages/custom_lint_builder/lib/src/client.dart b/packages/custom_lint_builder/lib/src/client.dart index c74b20fe..bfa5a0ee 100644 --- a/packages/custom_lint_builder/lib/src/client.dart +++ b/packages/custom_lint_builder/lib/src/client.dart @@ -9,7 +9,10 @@ import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; import 'package:analyzer/dart/analysis/context_root.dart'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/session.dart'; -import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; import 'package:analyzer/error/listener.dart'; import 'package:analyzer/file_system/file_system.dart'; import 'package:analyzer/file_system/physical_file_system.dart'; @@ -30,11 +33,14 @@ import 'package:custom_lint/src/v2/protocol.dart'; // ignore: implementation_imports, tight versioning import 'package:custom_lint_core/src/change_reporter.dart'; // ignore: implementation_imports, tight versioning -import 'package:custom_lint_core/src/node_lint_visitor.dart'; +import 'package:custom_lint_core/src/fixes.dart'; // ignore: implementation_imports, tight versioning import 'package:custom_lint_core/src/plugin_base.dart'; // ignore: implementation_imports, tight versioning import 'package:custom_lint_core/src/resolver.dart'; +// ignore: implementation_imports, tight versioning +import 'package:custom_lint_core/src/runnable.dart'; +import 'package:custom_lint_visitor/custom_lint_visitor.dart'; import 'package:glob/glob.dart'; import 'package:hotreloader/hotreloader.dart'; import 'package:meta/meta.dart'; @@ -91,23 +97,30 @@ Future _isVmServiceEnabled() async { } extension on analyzer_plugin.AnalysisErrorFixes { - bool canBatchFix(String filePath) { - final fixesExcludingIgnores = - fixes.where((change) => !change.isIgnoreChange).toList(); - - return fixesExcludingIgnores.length == 1 && - fixesExcludingIgnores.single.canBatchFix(filePath); + ({String id, int priority})? findBatchFix(String filePath) { + final fixToBatch = fixes + .where((change) => change.canBatchFix(filePath)) + // Only a single fix at a time can be batched. + .singleOrNull; + if (fixToBatch == null) return null; + final id = fixToBatch.change.id; + if (id == null) return null; + + return ( + id: id, + priority: fixToBatch.priority, + ); } } extension on analyzer_plugin.PrioritizedSourceChange { bool canBatchFix(String filePath) { - return change.edits.every((element) => element.file == filePath); + return !isIgnoreChange && + change.edits.every((element) => element.file == filePath); } bool get isIgnoreChange { - return change.id == IgnoreCode.ignoreForFileCode || - change.id == IgnoreCode.ignoreForLineCode; + return change.id == IgnoreCode.ignoreId; } } @@ -364,6 +377,26 @@ class _AnalysisErrorsKey { int get hashCode => Object.hash(filePath, analysisContext); } +class _FileContext { + _FileContext({ + required this.resolver, + required this.analysisContext, + required this.contextCollection, + required this.path, + required this.configs, + }) : key = _AnalysisErrorsKey( + filePath: path, + analysisContext: analysisContext, + ); + + final String path; + final _AnalysisErrorsKey key; + final CustomLintResolverImpl resolver; + final AnalysisContext analysisContext; + final AnalysisContextCollection contextCollection; + final _CustomLintAnalysisConfigs configs; +} + class _ClientAnalyzerPlugin extends analyzer_plugin.ServerPlugin { _ClientAnalyzerPlugin( this._channel, @@ -378,7 +411,7 @@ class _ClientAnalyzerPlugin extends analyzer_plugin.ServerPlugin { var _customLintConfigsForAnalysisContexts = {}; final _analysisErrorsForAnalysisContexts = - <_AnalysisErrorsKey, Iterable>{}; + <_AnalysisErrorsKey, List>{}; @override List get fileGlobsToAnalyze => ['*']; @@ -389,6 +422,27 @@ class _ClientAnalyzerPlugin extends analyzer_plugin.ServerPlugin { @override String get version => '1.0.0-alpha.0'; + Future<_FileContext?> _fileContext(String path) async { + final contextCollection = await _contextCollection.safeFirst; + final analysisContext = contextCollection.contextFor(path); + final resolver = analysisContext.createResolverForFile( + resourceProvider.getFile(path), + ); + + if (resolver == null) return null; + + final configs = _customLintConfigsForAnalysisContexts[analysisContext]; + if (configs == null) return null; + + return _FileContext( + path: path, + resolver: resolver, + analysisContext: analysisContext, + contextCollection: contextCollection, + configs: configs, + ); + } + Future reAnalyze() async { final contextCollection = _contextCollection.valueOrNull; if (contextCollection != null) { @@ -510,7 +564,7 @@ class _ClientAnalyzerPlugin extends analyzer_plugin.ServerPlugin { runPostRunCallbacks(postRunCallbacks); return analyzer_plugin.EditGetAssistsResult( - await changeReporter.waitForCompletion(), + await changeReporter.complete(), ); } @@ -545,121 +599,119 @@ class _ClientAnalyzerPlugin extends analyzer_plugin.ServerPlugin { Future handleEditGetFixes( analyzer_plugin.EditGetFixesParams parameters, ) async { - final contextCollection = await _contextCollection.safeFirst; - final analysisContext = contextCollection.contextFor(parameters.file); - final resolver = analysisContext.createResolverForFile( - resourceProvider.getFile(parameters.file), + final context = await _fileContext(parameters.file); + if (context == null) return analyzer_plugin.EditGetFixesResult([]); + + final analysisErrorsForContext = + _analysisErrorsForAnalysisContexts[context.key] ?? const []; + + final fixes = await _computeFixes( + analysisErrorsForContext + .where((error) => error.sourceRange.contains(parameters.offset)) + .toList(), + context, + analysisErrorsForContext, ); - if (resolver == null) return analyzer_plugin.EditGetFixesResult([]); - final key = _AnalysisErrorsKey( - filePath: parameters.file, - analysisContext: analysisContext, + return analyzer_plugin.EditGetFixesResult( + fixes.expand((fixes) { + return [ + fixes.fix, + if (fixes.batchFixes case final batchFixes?) batchFixes, + ]; + }).toList(), ); - final configs = _customLintConfigsForAnalysisContexts[analysisContext]; + } - final analysisErrorsForContext = - _analysisErrorsForAnalysisContexts[key] ?? const {}; - final errorsAtOffset = analysisErrorsForContext - .where( - (error) => - parameters.offset >= error.offset && - parameters.offset <= error.offset + error.length, - ) - .toList(); + Future< + List< + ({ + analyzer_plugin.AnalysisErrorFixes? batchFixes, + analyzer_plugin.AnalysisErrorFixes fix + })>> _computeFixes( + List errorsToFix, + _FileContext context, + List analysisErrorsForContext, + ) async { + return Future.wait( + errorsToFix.map((error) async { + final toBatch = analysisErrorsForContext + .where((e) => e.errorCode == error.errorCode) + .toList(); - if (errorsAtOffset.isEmpty || configs == null) { - return analyzer_plugin.EditGetFixesResult([]); - } + final changeReporterBuilder = ChangeReporterBuilderImpl( + context.resolver, + context.configs.analysisContext.currentSession, + ); - final allAnalysisErrorFixes = await Future.wait([ - for (final error in analysisErrorsForContext) - _handlesFixesForError( + await _runFixes( + context, error, analysisErrorsForContext, - configs, - resolver, - parameters, - ), - ]); - - final analysisErrorFixesForOffset = allAnalysisErrorFixes.nonNulls - .where( - (fix) => - parameters.offset >= fix.error.location.offset && - parameters.offset <= - fix.error.location.offset + fix.error.location.length, - ) - .toList(); + changeReporterBuilder: changeReporterBuilder, + ); + final fix = await changeReporterBuilder.completeAsFixes( + error, + context, + ); - final fixAll = {}; - for (final fix in allAnalysisErrorFixes) { - if (fix == null) continue; + final batchFix = fix.findBatchFix(context.path); + if (batchFix == null || toBatch.length <= 1) { + return ( + fix: fix, + batchFixes: null, + ); + } - final errorCode = fix.error.code; - fixAll.putIfAbsent(errorCode, () { - final analysisErrorsWithCode = allAnalysisErrorFixes.nonNulls - .where((fix) => fix.error.code == errorCode) - .toList(); + final batchReporter = ChangeReporterImpl( + context.configs.analysisContext.currentSession, + context.resolver, + ); - // Don't show "fix-all" unless at least two errors have the same code. - if (analysisErrorsWithCode.length < 2) return null; + final batchReporterBuilder = BatchChangeReporterBuilder( + batchReporter.createChangeBuilder( + message: 'Fix all "${error.errorCode}"', + priority: batchFix.priority - 1, + ), + ); - final fixesWithCode = analysisErrorsWithCode - .where((e) => e.canBatchFix(parameters.file)) - // Ignoring "ignore" fixes - .map((e) { - final fixesExcludingIgnores = - e.fixes.where((change) => !change.isIgnoreChange).toList(); + // Compute batch in sequential mode because ChangeBuilder requires it. + for (final toBatchError in toBatch) { + await _runFixes( + where: (fix) => fix.id == batchFix.id, + context, + toBatchError, + analysisErrorsForContext, + changeReporterBuilder: batchReporterBuilder, + sequential: true, + ); + } - return (fixes: fixesExcludingIgnores, error: e.error); - }).sorted( - (a, b) => b.error.location.offset - a.error.location.offset, - ); + final batchFixes = + await batchReporterBuilder.completeAsFixes(error, context); - // Don't show fix-all if there's no good fix. - if (fixesWithCode.isEmpty) return null; - - final priority = fixesWithCode - .expand((e) => e.fixes) - .map((e) => e.priority - 1) - .firstOrNull ?? - 0; - - return analyzer_plugin.AnalysisErrorFixes( - fix.error, - fixes: [ - analyzer_plugin.PrioritizedSourceChange( - priority, - analyzer_plugin.SourceChange( - 'Fix all "$errorCode"', - edits: fixesWithCode - .expand((e) => e.fixes) - .expand((e) => e.change.edits) - .toList(), - ), - ), - ], + return ( + fix: fix, + batchFixes: batchFixes, ); - }); - } - - return analyzer_plugin.EditGetFixesResult([ - ...analysisErrorFixesForOffset, - ...fixAll.values.nonNulls, - ]); + }), + ); } - Future _handlesFixesForError( + Future _runFixes( + _FileContext context, AnalysisError analysisError, - Iterable allErrors, - _CustomLintAnalysisConfigs configs, - CustomLintResolver resolver, - analyzer_plugin.EditGetFixesParams parameters, - ) async { - final fixesForError = configs.fixes[analysisError.errorCode]; - if (fixesForError == null || fixesForError.isEmpty) { - return null; + List allErrors, { + required ChangeReporterBuilder changeReporterBuilder, + bool sequential = false, + bool Function(Fix fix)? where, + }) async { + Iterable? fixesForError = + context.configs.fixes[analysisError.errorCode]; + if (fixesForError == null) return; + + if (where != null) { + fixesForError = fixesForError.where(where); } final otherErrors = allErrors @@ -670,83 +722,76 @@ class _ClientAnalyzerPlugin extends analyzer_plugin.ServerPlugin { ) .toList(); - final postRunCallbacks = []; - // TODO implement verbose mode to log lint duration - final registry = NodeLintRegistry(LintRegistry(), enableTiming: false); - final sharedState = {}; - - final changeReporter = ChangeReporterImpl( - configs.analysisContext.currentSession, - resolver, + await _run( + context, + fixesForError.map((fix) { + return ( + runnable: fix, + args: ( + reporter: changeReporterBuilder.createChangeReporter(id: fix.id), + analysisError: analysisError, + others: otherErrors, + ) + ); + }), + sequential: sequential, ); - await Future.wait([ - for (final fix in fixesForError) - _runFixStartup( - resolver, - fix, - CustomLintContext( - LintRuleNodeRegistry(registry, fix.runtimeType.toString()), - postRunCallbacks.add, - sharedState, - configs.pubspec, - ), - ), - ]); - await Future.wait([ - for (final fix in fixesForError) - _runFixRun( - resolver, - fix, - CustomLintContext( - LintRuleNodeRegistry(registry, fix.runtimeType.toString()), - postRunCallbacks.add, - sharedState, - configs.pubspec, - ), - changeReporter, - analysisError, - otherErrors, - ), - ]); + await changeReporterBuilder.waitForCompletion(); + } - runPostRunCallbacks(postRunCallbacks); + Future _run( + _FileContext context, + Iterable<({Runnable runnable, ArgsT args})> allRunnables, { + bool sequential = false, + }) async { + // TODO implement verbose mode to log lint duration - return analyzer_plugin.AnalysisErrorFixes( - CustomAnalyzerConverter().convertAnalysisError( - analysisError, - lineInfo: resolver.lineInfo, - severity: analysisError.errorCode.errorSeverity, - ), - fixes: await changeReporter.waitForCompletion(), - ); - } + final bundledRunnables = + sequential ? allRunnables.map((e) => [e]).toList() : [allRunnables]; - Future _runFixStartup( - CustomLintResolver resolver, - Fix fix, - CustomLintContext context, - ) async { - return _runLintZoned( - resolver, - () => fix.startUp(resolver, context), - name: fix.runtimeType.toString(), - ); - } + for (final runnableBundle in bundledRunnables) { + final registry = NodeLintRegistry(LintRegistry(), enableTiming: false); + final postRunCallbacks = []; + final sharedState = {}; - Future _runFixRun( - CustomLintResolver resolver, - Fix fix, - CustomLintContext context, - ChangeReporter changeReporter, - AnalysisError analysisError, - List others, - ) async { - return _runLintZoned( - resolver, - () => fix.run(resolver, changeReporter, context, analysisError, others), - name: fix.runtimeType.toString(), - ); + await Future.wait([ + for (final (:runnable, args: _) in runnableBundle) + _runLintZoned( + context.resolver, + () => runnable.startUp( + context.resolver, + CustomLintContext( + LintRuleNodeRegistry(registry, runnable.runtimeType.toString()), + postRunCallbacks.add, + sharedState, + context.configs.pubspec, + ), + ), + name: runnable.runtimeType.toString(), + ), + ]); + + await Future.wait([ + for (final (:runnable, :args) in runnableBundle) + _runLintZoned( + context.resolver, + () => runnable.callRun( + context.resolver, + CustomLintContext( + LintRuleNodeRegistry(registry, runnable.runtimeType.toString()), + postRunCallbacks.add, + sharedState, + context.configs.pubspec, + ), + args, + ), + name: runnable.runtimeType.toString(), + ), + ]); + + runPostRunCallbacks(postRunCallbacks); + } } @override @@ -966,41 +1011,30 @@ class _ClientAnalyzerPlugin extends analyzer_plugin.ServerPlugin { final fixedCodes = (Zone.current[#_fixedCodes] as Set?) ?? {}; - final allFixes = await _computeFixes( + final context = await _fileContext(path); + if (context == null) return false; + + final allFixes = await _computeFistBatchFixes( allAnalysisErrors, - resolver, - configs, + context, fixedCodes, path: path, - ).toList(); + ); if (allFixes.isEmpty) return false; final source = resolver.source.contents.data; - final firstFixCode = allFixes.first.analysisError.errorCode; - final didApplyAllFixes = - allFixes.every((e) => e.analysisError.errorCode == firstFixCode); try { - // Apply fixes from top to bottom. - allFixes.sort( - (a, b) => b.analysisError.offset - a.analysisError.offset, - ); - final editedSource = analyzer_plugin.SourceEdit.applySequence( source, - // We apply fixes only once at a time, to avoid conflicts. - // To do so, we take the first fixed lint code, and apply fixes - // only for that code. allFixes - .where((e) => e.analysisError.errorCode == firstFixCode) + .expand((e) => e.fixes) + .expand((e) => e.change.edits) .expand((e) => e.edits), ); - if (didApplyAllFixes) { - // Apply fixes to the file - io.File(path).writeAsStringSync(editedSource); - } + io.File(path).writeAsStringSync(editedSource); // Update in-memory file content before re-running analysis. resourceProvider.setOverlay( @@ -1017,7 +1051,7 @@ class _ClientAnalyzerPlugin extends analyzer_plugin.ServerPlugin { }, zoneValues: { // We update the list of fixed codes to avoid re-fixing the same lint - #_fixedCodes: {...fixedCodes, firstFixCode.name}, + #_fixedCodes: {...fixedCodes, ...allFixes.map((e) => e.error.code)}, }, ); } catch (e) { @@ -1027,39 +1061,26 @@ class _ClientAnalyzerPlugin extends analyzer_plugin.ServerPlugin { } } - Stream< - ({ - AnalysisError analysisError, - Iterable edits, - })> _computeFixes( + Future> _computeFistBatchFixes( List allAnalysisErrors, - CustomLintResolver resolver, - _CustomLintAnalysisConfigs configs, + _FileContext context, Set fixedCodes, { required String path, - }) async* { - if (!_client.fix) return; - - for (final analysisError in allAnalysisErrors) { - if (fixedCodes.contains(analysisError.errorCode.name)) continue; + }) async { + if (!_client.fix) return []; - final fixesForLint = await _handlesFixesForError( - analysisError, - allAnalysisErrors.toSet(), - configs, - resolver, - analyzer_plugin.EditGetFixesParams(path, analysisError.offset), - ); + final errorToFix = allAnalysisErrors + .where((e) => !fixedCodes.contains(e.errorCode.name)) + .firstOrNull; + if (errorToFix == null) return []; - if (fixesForLint == null || !fixesForLint.canBatchFix(resolver.path)) { - continue; - } + final fixes = await _computeFixes( + [errorToFix], + context, + allAnalysisErrors, + ); - yield ( - analysisError: analysisError, - edits: fixesForLint.fixes.single.change.edits.expand((e) => e.edits), - ); - } + return fixes.map((e) => e.batchFixes ?? e.fix).toList(); } /// Queue an operation to be awaited by [_awaitAnalysisDone] @@ -1201,3 +1222,19 @@ class _AnalysisErrorListenerDelegate implements AnalysisErrorListener { @override void onError(AnalysisError error) => _onError(error); } + +extension on ChangeReporterBuilder { + Future completeAsFixes( + AnalysisError analysisError, + _FileContext context, + ) async { + return analyzer_plugin.AnalysisErrorFixes( + CustomAnalyzerConverter().convertAnalysisError( + analysisError, + lineInfo: context.resolver.lineInfo, + severity: analysisError.errorCode.errorSeverity, + ), + fixes: await complete(), + ); + } +} diff --git a/packages/custom_lint_builder/lib/src/expect_lint.dart b/packages/custom_lint_builder/lib/src/expect_lint.dart index c69b7599..f110f371 100644 --- a/packages/custom_lint_builder/lib/src/expect_lint.dart +++ b/packages/custom_lint_builder/lib/src/expect_lint.dart @@ -1,4 +1,7 @@ -import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; import 'package:analyzer/error/listener.dart'; import 'package:analyzer/source/line_info.dart'; import 'package:meta/meta.dart'; diff --git a/packages/custom_lint_builder/lib/src/ignore.dart b/packages/custom_lint_builder/lib/src/ignore.dart index 8e48db53..b5d57b25 100644 --- a/packages/custom_lint_builder/lib/src/ignore.dart +++ b/packages/custom_lint_builder/lib/src/ignore.dart @@ -1,4 +1,7 @@ -import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; import 'package:collection/collection.dart'; import 'package:custom_lint_core/custom_lint_core.dart'; @@ -101,10 +104,10 @@ List parseIgnoreForFile(String source) { /// Built in fix to ignore a lint. class IgnoreCode extends DartFix { /// The code for 'ignore for line' fix. - static const ignoreForLineCode = 'ignore_for_line'; + static const ignoreId = '<>'; - /// The code for 'ignore for file' fix. - static const ignoreForFileCode = 'ignore_for_file'; + @override + String get id => ignoreId; @override void run( @@ -120,7 +123,6 @@ class IgnoreCode extends DartFix { final ignoreForLineChangeBuilder = reporter.createChangeBuilder( message: 'Ignore "${analysisError.errorCode.name}" for line', priority: 1, - id: ignoreForLineCode, ); ignoreForLineChangeBuilder.addDartFileEdit((builder) { @@ -149,7 +151,6 @@ class IgnoreCode extends DartFix { final ignoreForFileChangeBuilder = reporter.createChangeBuilder( message: 'Ignore "${analysisError.errorCode.name}" for file', priority: 0, - id: ignoreForFileCode, ); ignoreForFileChangeBuilder.addDartFileEdit((builder) { diff --git a/packages/custom_lint_builder/pubspec.yaml b/packages/custom_lint_builder/pubspec.yaml index 352f629e..131fb91b 100644 --- a/packages/custom_lint_builder/pubspec.yaml +++ b/packages/custom_lint_builder/pubspec.yaml @@ -16,6 +16,8 @@ dependencies: # Using tight constraints as custom_lint_builder communicate with each-other # using a specific contract custom_lint_core: 0.6.10 + # Using loose constraint to support a range of analyzer versions. + custom_lint_visitor: ^1.0.0 glob: ^2.1.1 hotreloader: ">=3.0.5 <5.0.0" meta: ^1.7.0 diff --git a/packages/custom_lint_builder/test/analyzer_converter_test.dart b/packages/custom_lint_builder/test/analyzer_converter_test.dart index c529adc3..654c7cb5 100644 --- a/packages/custom_lint_builder/test/analyzer_converter_test.dart +++ b/packages/custom_lint_builder/test/analyzer_converter_test.dart @@ -1,4 +1,7 @@ -import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; import 'package:analyzer/file_system/memory_file_system.dart'; import 'package:analyzer/source/file_source.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; diff --git a/packages/custom_lint_core/CHANGELOG.md b/packages/custom_lint_core/CHANGELOG.md index 8ee73af5..f0df11c3 100644 --- a/packages/custom_lint_core/CHANGELOG.md +++ b/packages/custom_lint_core/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased 0.7.0 + +- `custom_lint --fix` and the generated "Fix all " assists + now correctly handle imports. +- Now supports a broad number of analyzer version. + ## 0.6.10 - 2024-10-10 - Added support for `dart:io` imports when using `TypeChecker.fromPackage` (thanks to @oskar-zeinomahmalat-sonarsource) diff --git a/packages/custom_lint_core/lib/custom_lint_core.dart b/packages/custom_lint_core/lib/custom_lint_core.dart index 12260514..deab9b5f 100644 --- a/packages/custom_lint_core/lib/custom_lint_core.dart +++ b/packages/custom_lint_core/lib/custom_lint_core.dart @@ -1,12 +1,20 @@ +export 'package:custom_lint_visitor/custom_lint_visitor.dart' + hide LintRegistry, LinterVisitor, NodeLintRegistry; + export 'src/assist.dart'; -export 'src/change_reporter.dart' hide ChangeReporterImpl; +export 'src/change_reporter.dart' + hide + BatchChangeReporterBuilder, + BatchChangeReporterImpl, + ChangeBuilderImpl, + ChangeReporterBuilder, + ChangeReporterBuilderImpl, + ChangeReporterImpl; export 'src/configs.dart'; -export 'src/fixes.dart'; +export 'src/fixes.dart' hide FixArgs; export 'src/lint_codes.dart'; export 'src/lint_rule.dart'; export 'src/matcher.dart'; -export 'src/node_lint_visitor.dart' - hide LintRegistry, LinterVisitor, NodeLintRegistry; export 'src/package_utils.dart' hide FindProjectError; export 'src/plugin_base.dart' hide runPostRunCallbacks; export 'src/resolver.dart' hide CustomLintResolverImpl; diff --git a/packages/custom_lint_core/lib/src/assist.dart b/packages/custom_lint_core/lib/src/assist.dart index f3d3fa22..ba96444c 100644 --- a/packages/custom_lint_core/lib/src/assist.dart +++ b/packages/custom_lint_core/lib/src/assist.dart @@ -4,13 +4,13 @@ import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/source/source_range.dart'; import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:custom_lint_visitor/custom_lint_visitor.dart'; import 'package:meta/meta.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'change_reporter.dart'; import 'fixes.dart'; import 'lint_rule.dart'; -import 'node_lint_visitor.dart'; import 'plugin_base.dart'; import 'resolver.dart'; @@ -123,7 +123,7 @@ abstract class DartAssist extends Assist { run(resolver, reporter, context, target); runPostRunCallbacks(postRunCallbacks); - return reporter.waitForCompletion(); + return reporter.complete(); } /// Analyze a Dart file and runs this assist in test mode. diff --git a/packages/custom_lint_core/lib/src/change_reporter.dart b/packages/custom_lint_core/lib/src/change_reporter.dart index 57ba593c..33c0b348 100644 --- a/packages/custom_lint_core/lib/src/change_reporter.dart +++ b/packages/custom_lint_core/lib/src/change_reporter.dart @@ -19,27 +19,125 @@ abstract class ChangeReporter { ChangeBuilder createChangeBuilder({ required String message, required int priority, - String? id, }); + + /// Waits for all [ChangeBuilder] to fully compute the source changes. + Future waitForCompletion(); + + /// Waits for completion and obtains the changes. + /// + /// This life-cycle can only be called once per [ChangeReporter]. + Future> complete(); +} + +@internal +abstract class ChangeReporterBuilder { + ChangeReporter createChangeReporter({required String id}); + + Future> complete(); + + Future waitForCompletion(); +} + +@internal +class BatchChangeReporterBuilder extends ChangeReporterBuilder { + BatchChangeReporterBuilder(ChangeBuilderImpl batchBuilder) + : _reporter = BatchChangeReporterImpl(batchBuilder); + + final BatchChangeReporterImpl _reporter; + + @override + ChangeReporter createChangeReporter({required String id}) => _reporter; + + @override + Future waitForCompletion() => _reporter.waitForCompletion(); + + @override + Future> complete() => _reporter.complete(); +} + +@internal +class BatchChangeReporterImpl implements ChangeReporter { + BatchChangeReporterImpl(this.batchBuilder); + + final ChangeBuilderImpl batchBuilder; + + @override + ChangeBuilder createChangeBuilder({ + required String message, + required int priority, + String? id, + }) { + return batchBuilder; + } + + @override + Future waitForCompletion() async => batchBuilder.waitForCompletion(); + + @override + Future> complete() async { + return [await batchBuilder.complete()]; + } +} + +@internal +class ChangeReporterBuilderImpl extends ChangeReporterBuilder { + ChangeReporterBuilderImpl(this._resolver, this._analysisSession); + + final CustomLintResolver _resolver; + final AnalysisSession _analysisSession; + final List _reporters = []; + + @override + ChangeReporter createChangeReporter({required String id}) { + final reporter = ChangeReporterImpl( + _analysisSession, + _resolver, + id: id, + ); + _reporters.add(reporter); + + return reporter; + } + + @override + Future waitForCompletion() async { + await Future.wait( + _reporters.map((e) => e.waitForCompletion()), + ); + } + + @override + Future> complete() async { + final changes = Stream.fromFutures( + _reporters.map((e) => e.complete()), + ); + + return changes.expand((e) => e).toList(); + } } /// The implementation of [ChangeReporter] @internal class ChangeReporterImpl implements ChangeReporter { /// The implementation of [ChangeReporter] - ChangeReporterImpl(this._analysisSession, this._resolver); + ChangeReporterImpl( + this._analysisSession, + this._resolver, { + this.id, + }); final CustomLintResolver _resolver; final AnalysisSession _analysisSession; - final _changeBuilders = <_ChangeBuilderImpl>[]; + final _changeBuilders = []; + final String? id; @override - ChangeBuilder createChangeBuilder({ + ChangeBuilderImpl createChangeBuilder({ required String message, required int priority, - String? id, }) { - final changeBuilderImpl = _ChangeBuilderImpl( + final changeBuilderImpl = ChangeBuilderImpl( message, analysisSession: _analysisSession, priority: priority, @@ -51,11 +149,17 @@ class ChangeReporterImpl implements ChangeReporter { return changeBuilderImpl; } - /// Waits for all [ChangeBuilder] to fully compute the source changes. - @internal - Future> waitForCompletion() async { + @override + Future waitForCompletion() async { + await Future.wait( + _changeBuilders.map((e) => e.waitForCompletion()), + ); + } + + @override + Future> complete() async { return Future.wait( - _changeBuilders.map((e) => e._waitForCompletion()), + _changeBuilders.map((e) => e.complete()), ); } } @@ -106,8 +210,9 @@ abstract class ChangeBuilder { ); } -class _ChangeBuilderImpl implements ChangeBuilder { - _ChangeBuilderImpl( +@internal +class ChangeBuilderImpl implements ChangeBuilder { + ChangeBuilderImpl( this._message, { required this.path, required this.priority, @@ -121,6 +226,7 @@ class _ChangeBuilderImpl implements ChangeBuilder { final String path; final String? id; final analyzer_plugin.ChangeBuilder _innerChangeBuilder; + var _completed = false; final _operations = >[]; @override @@ -175,8 +281,17 @@ class _ChangeBuilderImpl implements ChangeBuilder { ); } - Future _waitForCompletion() async { + Future waitForCompletion() async { await Future.wait(_operations); + } + + Future complete() async { + if (_completed) { + throw StateError('Cannot call waitForCompletion more than once'); + } + _completed = true; + + await waitForCompletion(); return PrioritizedSourceChange( priority, diff --git a/packages/custom_lint_core/lib/src/fixes.dart b/packages/custom_lint_core/lib/src/fixes.dart index 00e2e053..5af24f37 100644 --- a/packages/custom_lint_core/lib/src/fixes.dart +++ b/packages/custom_lint_core/lib/src/fixes.dart @@ -2,28 +2,48 @@ import 'dart:io'; import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/utilities.dart'; -import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; import 'package:analyzer_plugin/protocol/protocol_generated.dart'; +import 'package:custom_lint_visitor/custom_lint_visitor.dart'; import 'package:meta/meta.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:uuid/uuid.dart'; import 'change_reporter.dart'; import 'lint_rule.dart'; -import 'node_lint_visitor.dart'; import 'plugin_base.dart'; import 'resolver.dart'; +import 'runnable.dart'; + +/// Args for [Fix]. +@internal +typedef FixArgs = ({ + ChangeReporter reporter, + AnalysisError analysisError, + List others, +}); + +const _uid = Uuid(); /// {@template custom_lint_builder.lint_rule} /// A base class for defining quick-fixes for a [LintRule] /// /// For creating assists inside Dart files, see [DartFix]. -/// Suclassing [Fix] can be helpful if you wish to implement assists for +/// Subclassing [Fix] can be helpful if you wish to implement assists for /// non-Dart files (yaml, json, ...) /// /// For usage information, see https://github.com/invertase/dart_custom_lint/blob/main/docs/fixes.md /// {@endtemplate} @immutable -abstract class Fix { +abstract class Fix extends Runnable { + /// A unique ID for a fix. Must be unique across all fixes of any package. + /// + /// This is used to know which fix triggered a change, for batch support. + late final String id = _uid.v4(); + /// A list of glob patterns matching the files that [run] cares about. /// /// This can include Dart files, Yaml files, ... @@ -32,11 +52,28 @@ abstract class Fix { /// Emits lints for a given file. /// /// [run] will only be invoked with files respecting [filesToAnalyze] + @override Future startUp( CustomLintResolver resolver, CustomLintContext context, ) async {} + @internal + @override + void callRun( + CustomLintResolver resolver, + CustomLintContext context, + FixArgs args, + ) { + run( + resolver, + args.reporter, + context, + args.analysisError, + args.others, + ); + } + /// Emits lints for a given file. /// /// [run] will only be invoked with files respecting [filesToAnalyze] @@ -119,7 +156,7 @@ abstract class DartFix extends Fix { run(resolver, reporter, context, analysisError, others); runPostRunCallbacks(postRunCallbacks); - return reporter.waitForCompletion(); + return reporter.complete(); } /// Analyze a Dart file and runs this fix in test mode. diff --git a/packages/custom_lint_core/lib/src/lint_codes.dart b/packages/custom_lint_core/lib/src/lint_codes.dart index 0e449389..c1c60cf5 100644 --- a/packages/custom_lint_core/lib/src/lint_codes.dart +++ b/packages/custom_lint_core/lib/src/lint_codes.dart @@ -4,7 +4,10 @@ // 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/error/error.dart' hide LintCode; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; import 'package:meta/meta.dart'; import '../custom_lint_core.dart'; diff --git a/packages/custom_lint_core/lib/src/lint_rule.dart b/packages/custom_lint_core/lib/src/lint_rule.dart index cfa84502..dff02af9 100644 --- a/packages/custom_lint_core/lib/src/lint_rule.dart +++ b/packages/custom_lint_core/lib/src/lint_rule.dart @@ -6,10 +6,10 @@ import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/error/error.dart' show AnalysisError; import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_visitor/custom_lint_visitor.dart'; import 'package:meta/meta.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import '../custom_lint_core.dart'; -import 'node_lint_visitor.dart'; import 'plugin_base.dart'; import 'resolver.dart'; diff --git a/packages/custom_lint_core/lib/src/runnable.dart b/packages/custom_lint_core/lib/src/runnable.dart new file mode 100644 index 00000000..a37194bd --- /dev/null +++ b/packages/custom_lint_core/lib/src/runnable.dart @@ -0,0 +1,21 @@ +import 'package:meta/meta.dart'; + +import 'lint_rule.dart'; +import 'resolver.dart'; + +/// A base-class for runnable objects. +abstract class Runnable { + /// Initializes the runnable object. + Future startUp( + CustomLintResolver resolver, + CustomLintContext context, + ); + + /// Runs the runnable object. + @internal + void callRun( + CustomLintResolver resolver, + CustomLintContext context, + RunArgs args, + ); +} diff --git a/packages/custom_lint_core/lib/src/source_range_extensions.dart b/packages/custom_lint_core/lib/src/source_range_extensions.dart index 2af234c7..0097c192 100644 --- a/packages/custom_lint_core/lib/src/source_range_extensions.dart +++ b/packages/custom_lint_core/lib/src/source_range_extensions.dart @@ -1,5 +1,8 @@ import 'package:analyzer/dart/ast/syntactic_entity.dart'; -import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; import 'package:analyzer/source/source_range.dart'; /// Adds [sourceRange] diff --git a/packages/custom_lint_core/pubspec.yaml b/packages/custom_lint_core/pubspec.yaml index 15cc73f3..4065c8c3 100644 --- a/packages/custom_lint_core/pubspec.yaml +++ b/packages/custom_lint_core/pubspec.yaml @@ -7,9 +7,10 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - analyzer: ^6.10.0 + analyzer: ^6.7.0 analyzer_plugin: ^0.11.0 collection: ^1.16.0 + custom_lint_visitor: ^1.0.0 glob: ^2.1.2 matcher: ^0.12.0 meta: ^1.7.0 @@ -17,6 +18,7 @@ dependencies: path: ^1.8.0 pubspec_parse: ^1.2.2 source_span: ^1.8.0 + uuid: ^4.5.1 yaml: ^3.1.1 dev_dependencies: diff --git a/packages/custom_lint_core/test/fix_test.dart b/packages/custom_lint_core/test/fix_test.dart index bf46ab84..cf344116 100644 --- a/packages/custom_lint_core/test/fix_test.dart +++ b/packages/custom_lint_core/test/fix_test.dart @@ -1,6 +1,9 @@ import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/utilities.dart'; -import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/error.dart' + hide + // ignore: undefined_hidden_name, Needed to support lower analyzer versions + LintCode; import 'package:custom_lint_core/src/change_reporter.dart'; import 'package:custom_lint_core/src/fixes.dart'; import 'package:custom_lint_core/src/lint_rule.dart'; diff --git a/packages/custom_lint_visitor/CHANGELOG.md b/packages/custom_lint_visitor/CHANGELOG.md new file mode 100644 index 00000000..43c4a891 --- /dev/null +++ b/packages/custom_lint_visitor/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0+ (6.7.0/6.11.0) + +Initial release diff --git a/packages/custom_lint_visitor/LICENSE b/packages/custom_lint_visitor/LICENSE new file mode 100644 index 00000000..3f58cd65 --- /dev/null +++ b/packages/custom_lint_visitor/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Invertase Limited + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/custom_lint_visitor/README.md b/packages/custom_lint_visitor/README.md new file mode 100644 index 00000000..2e02d64f --- /dev/null +++ b/packages/custom_lint_visitor/README.md @@ -0,0 +1,27 @@ +

+

custom_lint_core

+ An package exposing base classes for defining lint rules/fixes/assists. +

+ +

+ License +

+ +## About + +`custom_lint_visitor` is a dependency of `custom_lint`, for the sake of supporting +multiple Analyzer versions without causing too many breaking changes. + +It exposes various ways to traverse the tree of `AstNode`s using callbacks. + +## Versioning + +One version of `custom_lint_visitor` is released for every `analyzer` version. + +The version `1.0.0+6.7.0` means "Version 1.0.0 of custom_lint_visitor, for analyzer's 6.7.0 version". + +Whenever `custom_lint_visitor` is updated, a new version may be published for the same `analyzer` version. Such as `1.0.1+6.7.0` + +Depending on `custom_lint_visitor: ^1.0.0` will therefore support +any compatible Analyzer version. +To require a specific analyzer version, specify `analyzer: ` explicitly. \ No newline at end of file diff --git a/packages/custom_lint_visitor/build.yaml b/packages/custom_lint_visitor/build.yaml new file mode 100644 index 00000000..6cb1b3de --- /dev/null +++ b/packages/custom_lint_visitor/build.yaml @@ -0,0 +1,12 @@ +targets: + $default: + builders: + lint_visitor_generator: + enabled: true + generate_for: + include: + - "**/node_lint_visitor.dart" + source_gen|combining_builder: + options: + ignore_for_file: + - "type=lint" diff --git a/packages/custom_lint_visitor/lib/custom_lint_visitor.dart b/packages/custom_lint_visitor/lib/custom_lint_visitor.dart new file mode 100644 index 00000000..daf78aad --- /dev/null +++ b/packages/custom_lint_visitor/lib/custom_lint_visitor.dart @@ -0,0 +1 @@ +export 'src/node_lint_visitor.dart'; diff --git a/packages/custom_lint_core/lib/src/node_lint_visitor.dart b/packages/custom_lint_visitor/lib/src/node_lint_visitor.dart similarity index 94% rename from packages/custom_lint_core/lib/src/node_lint_visitor.dart rename to packages/custom_lint_visitor/lib/src/node_lint_visitor.dart index 45e6b13e..2c58302c 100644 --- a/packages/custom_lint_core/lib/src/node_lint_visitor.dart +++ b/packages/custom_lint_visitor/lib/src/node_lint_visitor.dart @@ -7,14 +7,11 @@ import 'dart:collection'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; -import 'package:meta/meta.dart'; - import 'pragmas.dart'; part 'node_lint_visitor.g.dart'; /// Manages lint timing. -@internal class LintRegistry { /// Dictionary mapping lints (by name) to timers. final Map timers = HashMap(); diff --git a/packages/custom_lint_core/lib/src/node_lint_visitor.g.dart b/packages/custom_lint_visitor/lib/src/node_lint_visitor.g.dart similarity index 99% rename from packages/custom_lint_core/lib/src/node_lint_visitor.g.dart rename to packages/custom_lint_visitor/lib/src/node_lint_visitor.g.dart index 4b9b0c92..76036518 100644 --- a/packages/custom_lint_core/lib/src/node_lint_visitor.g.dart +++ b/packages/custom_lint_visitor/lib/src/node_lint_visitor.g.dart @@ -9,10 +9,8 @@ part of 'node_lint_visitor.dart'; // ************************************************************************** /// The AST visitor that runs handlers for nodes from the [_registry]. -@internal class LinterVisitor extends GeneralizingAstVisitor { /// The AST visitor that runs handlers for nodes from the [_registry]. - @internal LinterVisitor(this._registry); final NodeLintRegistry _registry; @@ -1268,10 +1266,8 @@ class _Subscription { } /// The container to register visitors for separate AST node types. -@internal class NodeLintRegistry { /// The container to register visitors for separate AST node types. - @internal NodeLintRegistry(this._lintRegistry, {required bool enableTiming}) : _enableTiming = enableTiming; @@ -2663,10 +2659,8 @@ class NodeLintRegistry { class LintRuleNodeRegistry { LintRuleNodeRegistry(this.nodeLintRegistry, this.name); - @internal final NodeLintRegistry nodeLintRegistry; - @internal final String name; @preferInline diff --git a/packages/custom_lint_visitor/lib/src/pragmas.dart b/packages/custom_lint_visitor/lib/src/pragmas.dart new file mode 100644 index 00000000..5f169fb3 --- /dev/null +++ b/packages/custom_lint_visitor/lib/src/pragmas.dart @@ -0,0 +1,2 @@ +/// Alias for vm:prefer-inline +const preferInline = pragma('vm:prefer-inline'); diff --git a/packages/custom_lint_visitor/pubspec.yaml b/packages/custom_lint_visitor/pubspec.yaml new file mode 100644 index 00000000..a736789f --- /dev/null +++ b/packages/custom_lint_visitor/pubspec.yaml @@ -0,0 +1,16 @@ +name: custom_lint_visitor +version: 1.0.0+6.11.0 +description: A package that exports visitors for CustomLint. +repository: https://github.com/invertase/dart_custom_lint + +environment: + sdk: ">=3.0.0 <4.0.0" + +dependencies: + analyzer: 6.11.0 + +dev_dependencies: + build_runner: ^2.3.3 + lint_visitor_generator: + path: ../lint_visitor_generator + test: ^1.22.2 diff --git a/packages/custom_lint_visitor/pubspec_overrides.yaml b/packages/custom_lint_visitor/pubspec_overrides.yaml new file mode 100644 index 00000000..42255ad1 --- /dev/null +++ b/packages/custom_lint_visitor/pubspec_overrides.yaml @@ -0,0 +1,4 @@ +# melos_managed_dependency_overrides: lint_visitor_generator +dependency_overrides: + lint_visitor_generator: + path: ../lint_visitor_generator diff --git a/packages/lint_visitor_generator/lib/builder.dart b/packages/lint_visitor_generator/lib/builder.dart index cde3676b..14d4a6ff 100644 --- a/packages/lint_visitor_generator/lib/builder.dart +++ b/packages/lint_visitor_generator/lib/builder.dart @@ -56,10 +56,8 @@ class _Subscription { } /// The container to register visitors for separate AST node types. -@internal class NodeLintRegistry { /// The container to register visitors for separate AST node types. - @internal NodeLintRegistry(this._lintRegistry, {required bool enableTiming}) : _enableTiming = enableTiming; @@ -110,10 +108,8 @@ class NodeLintRegistry { ) { buffer.writeln(''' /// The AST visitor that runs handlers for nodes from the [_registry]. -@internal class LinterVisitor extends GeneralizingAstVisitor { /// The AST visitor that runs handlers for nodes from the [_registry]. - @internal LinterVisitor(this._registry); final NodeLintRegistry _registry; @@ -161,10 +157,8 @@ class LinterVisitor extends GeneralizingAstVisitor { class LintRuleNodeRegistry { LintRuleNodeRegistry(this.nodeLintRegistry, this.name); - @internal final NodeLintRegistry nodeLintRegistry; - @internal final String name; ''');