From c887250f8891c4d0c225d910ea584bd16b8bbd18 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sun, 4 Jan 2026 21:16:42 +0800 Subject: [PATCH 01/15] Rename Map annotation to MapTo The `Map` class name conflicts with the built-in `dart:core` Map type. Renaming to `MapTo` avoids the conflict while preserving the annotation's purpose of mapping to a database name. --- pub/orm/lib/schema.dart | 2 +- pub/orm/lib/src/schema/{map.dart => map_to.dart} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename pub/orm/lib/src/schema/{map.dart => map_to.dart} (80%) diff --git a/pub/orm/lib/schema.dart b/pub/orm/lib/schema.dart index e9a589eb..b7919c54 100644 --- a/pub/orm/lib/schema.dart +++ b/pub/orm/lib/schema.dart @@ -1,5 +1,5 @@ export 'src/schema/schema.dart'; export 'src/schema/model.dart'; export 'src/schema/relation.dart'; -export 'src/schema/map.dart'; +export 'src/schema/map_to.dart'; export 'src/schema/id.dart'; diff --git a/pub/orm/lib/src/schema/map.dart b/pub/orm/lib/src/schema/map_to.dart similarity index 80% rename from pub/orm/lib/src/schema/map.dart rename to pub/orm/lib/src/schema/map_to.dart index 752db217..d7a501c8 100644 --- a/pub/orm/lib/src/schema/map.dart +++ b/pub/orm/lib/src/schema/map_to.dart @@ -2,9 +2,9 @@ import 'package:meta/meta.dart'; /// Annotation to map a model or field to a database name. @immutable -final class Map { +final class MapTo { final String name; /// Creates a mapping annotation with the given database name. - const Map(this.name); + const MapTo(this.name); } From f34ad8178fc82ade41b3adced6cf88feec3da936 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sun, 4 Jan 2026 21:19:03 +0800 Subject: [PATCH 02/15] Implement basic analysis server plugin structure --- pub/orm/lib/main.dart | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/pub/orm/lib/main.dart b/pub/orm/lib/main.dart index 4079f807..a75463eb 100644 --- a/pub/orm/lib/main.dart +++ b/pub/orm/lib/main.dart @@ -1,22 +1,16 @@ -// import 'dart:async'; +import 'dart:async'; -// import 'package:analysis_server_plugin/registry.dart'; +import 'package:analysis_server_plugin/plugin.dart'; +import 'package:analysis_server_plugin/registry.dart'; -// // import 'src/analyzer/plugin.dart'; +class AnalysisPlugin extends Plugin { + @override + String get name => 'orm'; -// // mixin A on Plugin { -// // @override -// // Future register(PluginRegistry registry) async { -// // await super.register(registry); -// // } -// // } + @override + Future register(PluginRegistry registry) async { + // TODO: implement register + } +} -// // class AnalysisPlugin extends Plugin with SchemaAnalyzerPlugin, A { -// // @override -// // Future register(PluginRegistry registry) async { -// // await super.register(registry); -// // // TODO: implement register -// // } -// // } - -// // final plugin = AnalysisPlugin(); +final plugin = AnalysisPlugin(); From e2014ae5c60866a07cb980ccc86480ecd1101f6f Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sun, 4 Jan 2026 21:26:19 +0800 Subject: [PATCH 03/15] Add ORM playground with SQLite configuration - Add playground directory with pubspec and analysis options - Configure ORM plugin and SQLite provider - Remove unused SQLite annotations file - Update config to only support SQLite provider --- playground/analysis_options.yaml | 4 + playground/orm.config.dart | 3 + playground/pubspec.lock | 204 ++++++++++++++++++++++++ playground/pubspec.yaml | 12 ++ pub/orm/lib/config.dart | 2 +- pub/orm/lib/src/sqlite/annotations.dart | 1 - 6 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 playground/analysis_options.yaml create mode 100644 playground/orm.config.dart create mode 100644 playground/pubspec.lock create mode 100644 playground/pubspec.yaml delete mode 100644 pub/orm/lib/src/sqlite/annotations.dart diff --git a/playground/analysis_options.yaml b/playground/analysis_options.yaml new file mode 100644 index 00000000..2ba868f1 --- /dev/null +++ b/playground/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:lints/recommended.yaml +plugins: + orm: + path: ../pub/orm diff --git a/playground/orm.config.dart b/playground/orm.config.dart new file mode 100644 index 00000000..28fa6dea --- /dev/null +++ b/playground/orm.config.dart @@ -0,0 +1,3 @@ +import 'package:orm/config.dart'; + +const config = Config(provider: .sqlite, output: 'lib/generated'); diff --git a/playground/pubspec.lock b/playground/pubspec.lock new file mode 100644 index 00000000..44523775 --- /dev/null +++ b/playground/pubspec.lock @@ -0,0 +1,204 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "5b7468c326d2f8a4f630056404ca0d291ade42918f4a3c6233618e724f39da8e" + url: "https://pub.dev" + source: hosted + version: "92.0.0" + analysis_server_plugin: + dependency: transitive + description: + name: analysis_server_plugin + sha256: "44adba4d74a2541173bad4c11531d2a4d22810c29c5ddb458a38e9f4d0e5eac7" + url: "https://pub.dev" + source: hosted + version: "0.3.4" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "70e4b1ef8003c64793a9e268a551a82869a8a96f39deb73dea28084b0e8bf75e" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: "6645a029da947ffd823d98118f385d4bd26b54eb069c006b22e0b94e451814b5" + url: "https://pub.dev" + source: hosted + version: "0.13.11" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b + url: "https://pub.dev" + source: hosted + version: "3.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + lints: + dependency: "direct dev" + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + orm: + dependency: "direct main" + description: + path: "../pub/orm" + relative: true + source: path + version: "6.0.0-dev.1" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: f52385d4f73589977c80797e60fe51014f7f2b957b5e9a62c3f6ada439889249 + url: "https://pub.dev" + source: hosted + version: "1.2.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: ec709065bb2c911b336853b67f3732dd13e0336bd065cc2f1061d7610ddf45e3 + url: "https://pub.dev" + source: hosted + version: "2.2.3" +sdks: + dart: ">=3.10.1 <4.0.0" diff --git a/playground/pubspec.yaml b/playground/pubspec.yaml new file mode 100644 index 00000000..9be7fd39 --- /dev/null +++ b/playground/pubspec.yaml @@ -0,0 +1,12 @@ +name: playground +publish_to: none + +environment: + sdk: ^3.10.1 + +dependencies: + orm: + path: ../pub/orm + +dev_dependencies: + lints: ^6.0.0 diff --git a/pub/orm/lib/config.dart b/pub/orm/lib/config.dart index 43a68799..23680ef0 100644 --- a/pub/orm/lib/config.dart +++ b/pub/orm/lib/config.dart @@ -1,6 +1,6 @@ import 'package:meta/meta.dart'; -enum DatabaseProvider { sqlite, mysql, postgresql, sqlserver } +enum DatabaseProvider { sqlite } @immutable final class Config { diff --git a/pub/orm/lib/src/sqlite/annotations.dart b/pub/orm/lib/src/sqlite/annotations.dart deleted file mode 100644 index 8b137891..00000000 --- a/pub/orm/lib/src/sqlite/annotations.dart +++ /dev/null @@ -1 +0,0 @@ - From 634eecf121aeb93f2f8ee969e8f2107f7683e2bf Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sun, 4 Jan 2026 22:09:28 +0800 Subject: [PATCH 04/15] Add ORM analyzer plugin with config validation and fix The plugin now provides a lint rule that checks for the required `const config = Config(...);` in `orm.config.dart` files and offers a quick fix to add it when missing. --- playground/orm.config.dart | 2 - pub/orm/lib/main.dart | 13 +- pub/orm/lib/src/analyzer/assists/.gitkeep | 0 .../analyzer/fixes/config_required_fix.dart | 111 ++++++++++++++++++ .../analyzer/rules/config_required_rule.dart | 84 +++++++++++++ pub/orm/pubspec.yaml | 1 + 6 files changed, 205 insertions(+), 6 deletions(-) create mode 100644 pub/orm/lib/src/analyzer/assists/.gitkeep create mode 100644 pub/orm/lib/src/analyzer/fixes/config_required_fix.dart create mode 100644 pub/orm/lib/src/analyzer/rules/config_required_rule.dart diff --git a/playground/orm.config.dart b/playground/orm.config.dart index 28fa6dea..da59cb86 100644 --- a/playground/orm.config.dart +++ b/playground/orm.config.dart @@ -1,3 +1 @@ import 'package:orm/config.dart'; - -const config = Config(provider: .sqlite, output: 'lib/generated'); diff --git a/pub/orm/lib/main.dart b/pub/orm/lib/main.dart index a75463eb..2b017f51 100644 --- a/pub/orm/lib/main.dart +++ b/pub/orm/lib/main.dart @@ -1,15 +1,20 @@ -import 'dart:async'; - import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/registry.dart'; +import 'src/analyzer/fixes/config_required_fix.dart'; +import 'src/analyzer/rules/config_required_rule.dart'; + class AnalysisPlugin extends Plugin { @override String get name => 'orm'; @override - Future register(PluginRegistry registry) async { - // TODO: implement register + void register(PluginRegistry registry) { + registry.registerWarningRule(ConfigRequiredRule()); + registry.registerFixForRule( + ConfigRequiredRule.code, + AddConfigConstantFix.new, + ); } } diff --git a/pub/orm/lib/src/analyzer/assists/.gitkeep b/pub/orm/lib/src/analyzer/assists/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart new file mode 100644 index 00000000..653783ff --- /dev/null +++ b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart @@ -0,0 +1,111 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; + +class AddConfigConstantFix extends ResolvedCorrectionProducer { + static const FixKind _kind = FixKind( + 'orm.fix.addConfigConstant', + DartFixKindPriority.standard, + "Define ORM config: const config = Config(...)", + ); + + AddConfigConstantFix({required super.context}); + + @override + CorrectionApplicability get applicability => + CorrectionApplicability.singleLocation; + + @override + FixKind get fixKind => _kind; + + @override + Future compute(ChangeBuilder builder) async { + final eol = utils.endOfLine; + final configImport = _findConfigImport(unit); + final prefix = configImport?.prefix?.name; + final needsImport = configImport == null; + + final configInsertOffset = _configInsertOffset(unit); + final importInsertOffset = _importInsertOffset(unit); + final configText = _buildConfigText(prefix, eol); + final importText = _buildImportText(eol); + + await builder.addDartFileEdit(file, (builder) { + if (needsImport && importInsertOffset == configInsertOffset) { + builder.addInsertion(importInsertOffset, (builder) { + builder.write(importText); + builder.write(eol); + builder.write(configText); + }); + return; + } + + if (needsImport) { + builder.addInsertion(importInsertOffset, (builder) { + builder.write(importText); + }); + } + + builder.addInsertion(configInsertOffset, (builder) { + if (configInsertOffset != 0 && needsImport == false) { + builder.write(eol); + } + builder.write(configText); + }); + }); + } + + ImportDirective? _findConfigImport(CompilationUnit unit) { + for (final directive in unit.directives) { + if (directive is! ImportDirective) { + continue; + } + final uri = directive.uri.stringValue; + if (uri == 'package:orm/config.dart') { + return directive; + } + } + return null; + } + + int _configInsertOffset(CompilationUnit unit) { + if (unit.directives.isEmpty) { + return 0; + } + return utils.getLineNext(unit.directives.last.end); + } + + int _importInsertOffset(CompilationUnit unit) { + ImportDirective? lastImport; + LibraryDirective? libraryDirective; + for (final directive in unit.directives) { + if (directive is LibraryDirective) { + libraryDirective = directive; + } else if (directive is ImportDirective) { + lastImport = directive; + } + } + + final anchor = lastImport ?? libraryDirective; + if (anchor == null) { + return 0; + } + return utils.getLineNext(anchor.end); + } + + String _buildImportText(String eol) => + "import 'package:orm/config.dart';$eol"; + + String _buildConfigText(String? prefix, String eol) { + final qualifier = (prefix == null || prefix.isEmpty) ? '' : '$prefix.'; + return [ + 'const config = ${qualifier}Config(', + '${utils.oneIndent}provider: $qualifier${qualifier.isNotEmpty ? 'DatabaseProvider' : ''}.sqlite,', + "${utils.oneIndent}output: '', // TODO: update output path", + ');', + '', + ].join(eol); + } +} diff --git a/pub/orm/lib/src/analyzer/rules/config_required_rule.dart b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart new file mode 100644 index 00000000..3448a282 --- /dev/null +++ b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart @@ -0,0 +1,84 @@ +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; + +class ConfigRequiredRule extends AnalysisRule { + static const LintCode code = LintCode( + 'orm_config_required', + "Missing required 'const config = Config(...);' in orm.config.dart.", + correctionMessage: + "Add a top-level 'const config = Config(...);' to orm.config.dart.", + ); + + ConfigRequiredRule() + : super( + name: 'orm_config_required', + description: + 'Ensures orm.config.dart defines a top-level const Config.', + ); + + @override + LintCode get diagnosticCode => code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + registry.addCompilationUnit(this, _Visitor(this, context)); + } +} + +class _Visitor extends SimpleAstVisitor { + final AnalysisRule rule; + final RuleContext context; + + _Visitor(this.rule, this.context); + + @override + void visitCompilationUnit(CompilationUnit node) { + final currentUnit = context.currentUnit; + final packageRoot = context.package?.root; + if (currentUnit == null || packageRoot == null) { + return; + } + + final configFile = packageRoot.getChildAssumingFile('orm.config.dart'); + if (currentUnit.file.path != configFile.path) { + return; + } + + if (!_hasRequiredConfig(node)) { + rule.reportAtNode(node); + } + } + + bool _hasRequiredConfig(CompilationUnit unit) { + for (final declaration in unit.declarations) { + if (declaration is! TopLevelVariableDeclaration) { + continue; + } + + final variables = declaration.variables; + if (!variables.isConst) { + continue; + } + + for (final variable in variables.variables) { + if (variable.name.lexeme != 'config') { + continue; + } + + final initializer = variable.initializer; + if (initializer is InstanceCreationExpression && + initializer.constructorName.type.name.lexeme == 'Config') { + return true; + } + } + } + return false; + } +} diff --git a/pub/orm/pubspec.yaml b/pub/orm/pubspec.yaml index ab09b508..d71ae321 100644 --- a/pub/orm/pubspec.yaml +++ b/pub/orm/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: analyzer: ^9.0.0 analysis_server_plugin: ^0.3.4 meta: ^1.17.0 + analyzer_plugin: ^0.13.11 dev_dependencies: lints: ^6.0.0 From 714d07bb18d97376db124782a3fdccc7bbba2c04 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sun, 4 Jan 2026 22:26:13 +0800 Subject: [PATCH 05/15] Rename fix and improve config detection - Rename AddConfigConstantFix to ConfigRequiredFix - Add check to skip fix if config variable already exists - Improve rule detection to handle imported Config types - Set rule severity to ERROR --- pub/orm/lib/main.dart | 5 +- .../analyzer/fixes/config_required_fix.dart | 24 ++++++- .../analyzer/rules/config_required_rule.dart | 72 ++++++++++++++++++- 3 files changed, 93 insertions(+), 8 deletions(-) diff --git a/pub/orm/lib/main.dart b/pub/orm/lib/main.dart index 2b017f51..c63aed11 100644 --- a/pub/orm/lib/main.dart +++ b/pub/orm/lib/main.dart @@ -11,10 +11,7 @@ class AnalysisPlugin extends Plugin { @override void register(PluginRegistry registry) { registry.registerWarningRule(ConfigRequiredRule()); - registry.registerFixForRule( - ConfigRequiredRule.code, - AddConfigConstantFix.new, - ); + registry.registerFixForRule(ConfigRequiredRule.code, ConfigRequiredFix.new); } } diff --git a/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart index 653783ff..4a6a2c57 100644 --- a/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart +++ b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart @@ -4,14 +4,14 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; -class AddConfigConstantFix extends ResolvedCorrectionProducer { +class ConfigRequiredFix extends ResolvedCorrectionProducer { static const FixKind _kind = FixKind( - 'orm.fix.addConfigConstant', + 'orm.fix.config_required', DartFixKindPriority.standard, "Define ORM config: const config = Config(...)", ); - AddConfigConstantFix({required super.context}); + ConfigRequiredFix({required super.context}); @override CorrectionApplicability get applicability => @@ -22,6 +22,10 @@ class AddConfigConstantFix extends ResolvedCorrectionProducer { @override Future compute(ChangeBuilder builder) async { + if (_hasConfigVariable(unit)) { + return; + } + final eol = utils.endOfLine; final configImport = _findConfigImport(unit); final prefix = configImport?.prefix?.name; @@ -70,6 +74,20 @@ class AddConfigConstantFix extends ResolvedCorrectionProducer { return null; } + bool _hasConfigVariable(CompilationUnit unit) { + for (final declaration in unit.declarations) { + if (declaration is! TopLevelVariableDeclaration) { + continue; + } + for (final variable in declaration.variables.variables) { + if (variable.name.lexeme == 'config') { + return true; + } + } + } + return false; + } + int _configInsertOffset(CompilationUnit unit) { if (unit.directives.isEmpty) { return 0; diff --git a/pub/orm/lib/src/analyzer/rules/config_required_rule.dart b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart index 3448a282..42b92ecb 100644 --- a/pub/orm/lib/src/analyzer/rules/config_required_rule.dart +++ b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart @@ -11,6 +11,7 @@ class ConfigRequiredRule extends AnalysisRule { "Missing required 'const config = Config(...);' in orm.config.dart.", correctionMessage: "Add a top-level 'const config = Config(...);' to orm.config.dart.", + severity: DiagnosticSeverity.ERROR, ); ConfigRequiredRule() @@ -57,6 +58,8 @@ class _Visitor extends SimpleAstVisitor { } bool _hasRequiredConfig(CompilationUnit unit) { + final configImport = _findConfigImport(unit); + final hasLocalConfig = _hasLocalConfigDeclaration(unit); for (final declaration in unit.declarations) { if (declaration is! TopLevelVariableDeclaration) { continue; @@ -74,11 +77,78 @@ class _Visitor extends SimpleAstVisitor { final initializer = variable.initializer; if (initializer is InstanceCreationExpression && - initializer.constructorName.type.name.lexeme == 'Config') { + _isOrmConfigInitializer( + initializer, + configImport, + hasLocalConfig, + )) { return true; } } } return false; } + + ImportDirective? _findConfigImport(CompilationUnit unit) { + for (final directive in unit.directives) { + if (directive is! ImportDirective) { + continue; + } + final uri = directive.uri.stringValue; + if (uri == 'package:orm/config.dart') { + return directive; + } + } + return null; + } + + bool _hasLocalConfigDeclaration(CompilationUnit unit) { + for (final declaration in unit.declarations) { + if (declaration is FunctionDeclaration) { + continue; + } + if (declaration is NamedCompilationUnitMember && + declaration.name.lexeme == 'Config') { + return true; + } + } + return false; + } + + bool _isOrmConfigInitializer( + InstanceCreationExpression initializer, + ImportDirective? configImport, + bool hasLocalConfig, + ) { + if (initializer.constructorName.type.name.lexeme != 'Config') { + return false; + } + + final ctorElement = initializer.constructorName.element; + final classElement = ctorElement?.enclosingElement; + final library = classElement?.library; + final libraryUri = library?.firstFragment.source.uri; + if (libraryUri != null) { + return libraryUri.scheme == 'package' && + libraryUri.path == 'orm/config.dart'; + } + + if (configImport == null) { + return false; + } + + final importPrefix = configImport.prefix?.name; + final usagePrefix = + initializer.constructorName.type.importPrefix?.name.lexeme; + + if (importPrefix != null) { + return usagePrefix == importPrefix; + } + + if (usagePrefix != null) { + return false; + } + + return !hasLocalConfig; + } } From d231b02a52649de1b7eb23cc8d700fcba25426d7 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sun, 4 Jan 2026 22:56:01 +0800 Subject: [PATCH 06/15] Add analyzer tests and refactor config fixes - Add test_reflective_loader and analyzer_testing dependencies - Create README for analyzer plugin layout - Extract common config fix logic into base class - Add config replacement fix for existing config variables - Add unit tests for config required rule and fix kinds --- playground/orm.config.dart | 5 + pub/orm/lib/main.dart | 5 + pub/orm/lib/src/analyzer/README.md | 25 +++ .../lib/src/analyzer/fixes/_config_fix.dart | 157 ++++++++++++++++++ .../analyzer/fixes/config_required_fix.dart | 107 +----------- .../fixes/config_required_replace_fix.dart | 37 +++++ pub/orm/pubspec.yaml | 2 + .../analyzer/config_required_fix_test.dart | 15 ++ .../analyzer/config_required_rule_test.dart | 96 +++++++++++ pubspec.lock | 16 ++ 10 files changed, 363 insertions(+), 102 deletions(-) create mode 100644 pub/orm/lib/src/analyzer/README.md create mode 100644 pub/orm/lib/src/analyzer/fixes/_config_fix.dart create mode 100644 pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart create mode 100644 pub/orm/test/analyzer/config_required_fix_test.dart create mode 100644 pub/orm/test/analyzer/config_required_rule_test.dart diff --git a/playground/orm.config.dart b/playground/orm.config.dart index da59cb86..585b8451 100644 --- a/playground/orm.config.dart +++ b/playground/orm.config.dart @@ -1 +1,6 @@ import 'package:orm/config.dart'; + +const config = Config( + provider: DatabaseProvider.sqlite, + output: '', // TODO: update output path +); diff --git a/pub/orm/lib/main.dart b/pub/orm/lib/main.dart index c63aed11..415eb44e 100644 --- a/pub/orm/lib/main.dart +++ b/pub/orm/lib/main.dart @@ -2,6 +2,7 @@ import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/registry.dart'; import 'src/analyzer/fixes/config_required_fix.dart'; +import 'src/analyzer/fixes/config_required_replace_fix.dart'; import 'src/analyzer/rules/config_required_rule.dart'; class AnalysisPlugin extends Plugin { @@ -12,6 +13,10 @@ class AnalysisPlugin extends Plugin { void register(PluginRegistry registry) { registry.registerWarningRule(ConfigRequiredRule()); registry.registerFixForRule(ConfigRequiredRule.code, ConfigRequiredFix.new); + registry.registerFixForRule( + ConfigRequiredRule.code, + ConfigRequiredReplaceFix.new, + ); } } diff --git a/pub/orm/lib/src/analyzer/README.md b/pub/orm/lib/src/analyzer/README.md new file mode 100644 index 00000000..e3ef1149 --- /dev/null +++ b/pub/orm/lib/src/analyzer/README.md @@ -0,0 +1,25 @@ +# Analyzer plugin layout + +This directory holds analyzer-plugin logic for the ORM package. + +Structure +- `rules/`: analysis rules (diagnostics). +- `fixes/`: quick fixes for rule diagnostics. +- `assists/`: assists not tied to diagnostics. + +Naming +- Rule file: `*_rule.dart` (class `*Rule`). +- Fix file: `*_fix.dart` (class `*Fix`). +- Assist file: `*_assist.dart` (class `*Assist`). + +Identifiers +- Rule name: `orm_`. +- Fix id: `orm.fix.`. +- Assist id: `orm.assist.`. + +Registration +- Register rules and fixes in `lib/main.dart` via `PluginRegistry`. + +Tests +- Place tests under `test/analyzer/`. +- Use `analyzer_testing` + `test_reflective_loader` for rule tests. diff --git a/pub/orm/lib/src/analyzer/fixes/_config_fix.dart b/pub/orm/lib/src/analyzer/fixes/_config_fix.dart new file mode 100644 index 00000000..15def782 --- /dev/null +++ b/pub/orm/lib/src/analyzer/fixes/_config_fix.dart @@ -0,0 +1,157 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; + +abstract class ConfigFix extends ResolvedCorrectionProducer { + ConfigFix({required super.context}); + + ImportDirective? findConfigImport(CompilationUnit unit) { + for (final directive in unit.directives) { + if (directive is! ImportDirective) { + continue; + } + final uri = directive.uri.stringValue; + if (uri == 'package:orm/config.dart') { + return directive; + } + } + return null; + } + + ConfigVariableInfo? findConfigVariable(CompilationUnit unit) { + for (final declaration in unit.declarations) { + if (declaration is! TopLevelVariableDeclaration) { + continue; + } + for (final variable in declaration.variables.variables) { + if (variable.name.lexeme == 'config') { + return ConfigVariableInfo(declaration, variable); + } + } + } + return null; + } + + int configInsertOffset(CompilationUnit unit) { + if (unit.directives.isEmpty) { + return 0; + } + return utils.getLineNext(unit.directives.last.end); + } + + int importInsertOffset(CompilationUnit unit) { + ImportDirective? lastImport; + LibraryDirective? libraryDirective; + for (final directive in unit.directives) { + if (directive is LibraryDirective) { + libraryDirective = directive; + } else if (directive is ImportDirective) { + lastImport = directive; + } + } + + final anchor = lastImport ?? libraryDirective; + if (anchor == null) { + return 0; + } + return utils.getLineNext(anchor.end); + } + + String buildImportText(String eol) => "import 'package:orm/config.dart';$eol"; + + String buildConfigText(String? prefix, String eol) { + final qualifier = (prefix == null || prefix.isEmpty) ? '' : '$prefix.'; + final provider = qualifier.isNotEmpty + ? '${qualifier}DatabaseProvider.sqlite' + : '.sqlite'; + return [ + 'const config = ${qualifier}Config(', + '${utils.oneIndent}$provider,', + "${utils.oneIndent}output: '', // TODO: update output path", + ');', + ].join(eol); + } + + Future insertConfig( + ChangeBuilder builder, { + required int configDeclOffset, + }) async { + final eol = utils.endOfLine; + final configImport = findConfigImport(unit); + final prefix = configImport?.prefix?.name; + final needsImport = configImport == null; + + final importOffset = importInsertOffset(unit); + final configText = buildConfigText(prefix, eol); + final importText = buildImportText(eol); + + await builder.addDartFileEdit(file, (builder) { + if (needsImport && importOffset == configDeclOffset) { + builder.addInsertion(importOffset, (builder) { + builder.write(importText); + builder.write(eol); + builder.write(configText); + }); + return; + } + + if (needsImport) { + builder.addInsertion(importOffset, (builder) { + builder.write(importText); + }); + } + + builder.addInsertion(configDeclOffset, (builder) { + builder.write(configText); + }); + }); + } + + Future replaceConfig( + ChangeBuilder builder, { + required ConfigVariableInfo info, + }) async { + final eol = utils.endOfLine; + final configImport = findConfigImport(unit); + final prefix = configImport?.prefix?.name; + final needsImport = configImport == null; + + final importOffset = importInsertOffset(unit); + final configText = buildConfigText(prefix, eol); + final importText = buildImportText(eol); + + final declaration = info.declaration; + final replaceRange = SourceRange(declaration.offset, declaration.length); + + await builder.addDartFileEdit(file, (builder) { + if (needsImport && importOffset == replaceRange.offset) { + builder.addReplacement(replaceRange, (builder) { + builder.write(importText); + builder.write(eol); + builder.write(configText); + }); + return; + } + + if (needsImport) { + builder.addInsertion(importOffset, (builder) { + builder.write(importText); + }); + } + + builder.addReplacement(replaceRange, (builder) { + builder.write(configText); + }); + }); + } +} + +class ConfigVariableInfo { + final TopLevelVariableDeclaration declaration; + final VariableDeclaration variable; + + ConfigVariableInfo(this.declaration, this.variable); + + bool get isSingle => declaration.variables.variables.length == 1; +} diff --git a/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart index 4a6a2c57..c72c2f68 100644 --- a/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart +++ b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart @@ -1,10 +1,11 @@ import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; -import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; -class ConfigRequiredFix extends ResolvedCorrectionProducer { +import '_config_fix.dart'; + +class ConfigRequiredFix extends ConfigFix { static const FixKind _kind = FixKind( 'orm.fix.config_required', DartFixKindPriority.standard, @@ -22,108 +23,10 @@ class ConfigRequiredFix extends ResolvedCorrectionProducer { @override Future compute(ChangeBuilder builder) async { - if (_hasConfigVariable(unit)) { + if (findConfigVariable(unit) != null) { return; } - final eol = utils.endOfLine; - final configImport = _findConfigImport(unit); - final prefix = configImport?.prefix?.name; - final needsImport = configImport == null; - - final configInsertOffset = _configInsertOffset(unit); - final importInsertOffset = _importInsertOffset(unit); - final configText = _buildConfigText(prefix, eol); - final importText = _buildImportText(eol); - - await builder.addDartFileEdit(file, (builder) { - if (needsImport && importInsertOffset == configInsertOffset) { - builder.addInsertion(importInsertOffset, (builder) { - builder.write(importText); - builder.write(eol); - builder.write(configText); - }); - return; - } - - if (needsImport) { - builder.addInsertion(importInsertOffset, (builder) { - builder.write(importText); - }); - } - - builder.addInsertion(configInsertOffset, (builder) { - if (configInsertOffset != 0 && needsImport == false) { - builder.write(eol); - } - builder.write(configText); - }); - }); - } - - ImportDirective? _findConfigImport(CompilationUnit unit) { - for (final directive in unit.directives) { - if (directive is! ImportDirective) { - continue; - } - final uri = directive.uri.stringValue; - if (uri == 'package:orm/config.dart') { - return directive; - } - } - return null; - } - - bool _hasConfigVariable(CompilationUnit unit) { - for (final declaration in unit.declarations) { - if (declaration is! TopLevelVariableDeclaration) { - continue; - } - for (final variable in declaration.variables.variables) { - if (variable.name.lexeme == 'config') { - return true; - } - } - } - return false; - } - - int _configInsertOffset(CompilationUnit unit) { - if (unit.directives.isEmpty) { - return 0; - } - return utils.getLineNext(unit.directives.last.end); - } - - int _importInsertOffset(CompilationUnit unit) { - ImportDirective? lastImport; - LibraryDirective? libraryDirective; - for (final directive in unit.directives) { - if (directive is LibraryDirective) { - libraryDirective = directive; - } else if (directive is ImportDirective) { - lastImport = directive; - } - } - - final anchor = lastImport ?? libraryDirective; - if (anchor == null) { - return 0; - } - return utils.getLineNext(anchor.end); - } - - String _buildImportText(String eol) => - "import 'package:orm/config.dart';$eol"; - - String _buildConfigText(String? prefix, String eol) { - final qualifier = (prefix == null || prefix.isEmpty) ? '' : '$prefix.'; - return [ - 'const config = ${qualifier}Config(', - '${utils.oneIndent}provider: $qualifier${qualifier.isNotEmpty ? 'DatabaseProvider' : ''}.sqlite,', - "${utils.oneIndent}output: '', // TODO: update output path", - ');', - '', - ].join(eol); + await insertConfig(builder, configDeclOffset: configInsertOffset(unit)); } } diff --git a/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart b/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart new file mode 100644 index 00000000..8b45134c --- /dev/null +++ b/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart @@ -0,0 +1,37 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; + +import '_config_fix.dart'; + +class ConfigRequiredReplaceFix extends ConfigFix { + static const FixKind _kind = FixKind( + 'orm.fix.config_required_replace', + DartFixKindPriority.standard, + "Replace ORM config: const config = Config(...)", + ); + + ConfigRequiredReplaceFix({required super.context}); + + @override + CorrectionApplicability get applicability => + CorrectionApplicability.singleLocation; + + @override + FixKind get fixKind => _kind; + + @override + Future compute(ChangeBuilder builder) async { + final info = findConfigVariable(unit); + if (info == null) { + return; + } + + if (!info.isSingle || info.declaration.metadata.isNotEmpty) { + return; + } + + await replaceConfig(builder, info: info); + } +} diff --git a/pub/orm/pubspec.yaml b/pub/orm/pubspec.yaml index d71ae321..59cfa70c 100644 --- a/pub/orm/pubspec.yaml +++ b/pub/orm/pubspec.yaml @@ -14,5 +14,7 @@ dependencies: analyzer_plugin: ^0.13.11 dev_dependencies: + analyzer_testing: ^0.1.7 lints: ^6.0.0 test: ^1.25.6 + test_reflective_loader: ^0.4.0 diff --git a/pub/orm/test/analyzer/config_required_fix_test.dart b/pub/orm/test/analyzer/config_required_fix_test.dart new file mode 100644 index 00000000..b604675e --- /dev/null +++ b/pub/orm/test/analyzer/config_required_fix_test.dart @@ -0,0 +1,15 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:orm/src/analyzer/fixes/config_required_fix.dart'; +import 'package:orm/src/analyzer/fixes/config_required_replace_fix.dart'; +import 'package:test/test.dart'; + +void main() { + test('fix kind ids', () { + final context = StubCorrectionProducerContext.instance; + final fix = ConfigRequiredFix(context: context); + final replaceFix = ConfigRequiredReplaceFix(context: context); + + expect(fix.fixKind.id, 'orm.fix.config_required'); + expect(replaceFix.fixKind.id, 'orm.fix.config_required_replace'); + }); +} diff --git a/pub/orm/test/analyzer/config_required_rule_test.dart b/pub/orm/test/analyzer/config_required_rule_test.dart new file mode 100644 index 00000000..2b5331c4 --- /dev/null +++ b/pub/orm/test/analyzer/config_required_rule_test.dart @@ -0,0 +1,96 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:analyzer/src/lint/registry.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:orm/src/analyzer/rules/config_required_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +@reflectiveTest +class ConfigRequiredRuleTest extends AnalysisRuleTest { + @override + String get analysisRule => 'orm_config_required'; + + @override + void setUp() { + Registry.ruleRegistry.registerWarningRule(ConfigRequiredRule()); + super.setUp(); + newPubspecYamlFile(testPackageRootPath, 'name: orm\n'); + newFile(join(testPackageRootPath, 'lib', 'config.dart'), r''' +enum DatabaseProvider { sqlite } + +class Config { + final DatabaseProvider provider; + final String output; + + const Config({required this.provider, required this.output}); +} +'''); + } + + Future _assertMissingConfig(String content) async { + final path = join(testPackageRootPath, 'orm.config.dart'); + newFile(path, content); + await assertDiagnosticsInFile(path, [lint(0, content.length)]); + } + + Future _assertValidConfig(String content) async { + final path = join(testPackageRootPath, 'orm.config.dart'); + newFile(path, content); + await assertNoDiagnosticsInFile(path); + } + + void test_missingConfig() async { + await _assertMissingConfig(r''' +import 'package:orm/config.dart'; +'''); + } + + void test_validConfig() async { + await _assertValidConfig(r''' +import 'package:orm/config.dart'; + +const config = Config( + provider: DatabaseProvider.sqlite, + output: '', +); +'''); + } + + void test_prefixedImport() async { + await _assertValidConfig(r''' +import 'package:orm/config.dart' as orm; + +const config = orm.Config( + provider: orm.DatabaseProvider.sqlite, + output: '', +); +'''); + } + + void test_localConfigClass() async { + await _assertMissingConfig(r''' +class Config { + const Config(); +} + +const config = Config(); +'''); + } + + void test_notConst() async { + await _assertMissingConfig(r''' +import 'package:orm/config.dart'; + +final config = Config( + provider: DatabaseProvider.sqlite, + output: '', +); +'''); + } +} + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(ConfigRequiredRuleTest); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 3c375cd5..79307b81 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.13.11" + analyzer_testing: + dependency: transitive + description: + name: analyzer_testing + sha256: "900f868c2391080f4877ee2eef69e80c47202f0f9321618a04acdbad5c93c69b" + url: "https://pub.dev" + source: hosted + version: "0.1.7" args: dependency: transitive description: @@ -345,6 +353,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.14" + test_reflective_loader: + dependency: transitive + description: + name: test_reflective_loader + sha256: d828d5ca15179aaac2aaf8f510cf0a52ec28e0031681b044ec5e581a4b8002e7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" typed_data: dependency: transitive description: From 89f1e849c3b3f384910dd8ff275b2bad0d590126 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sun, 4 Jan 2026 23:01:20 +0800 Subject: [PATCH 07/15] Update config provider syntax to use named parameter --- playground/orm.config.dart | 2 +- pub/orm/lib/src/analyzer/fixes/_config_fix.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/playground/orm.config.dart b/playground/orm.config.dart index 585b8451..6ffb183a 100644 --- a/playground/orm.config.dart +++ b/playground/orm.config.dart @@ -1,6 +1,6 @@ import 'package:orm/config.dart'; const config = Config( - provider: DatabaseProvider.sqlite, + provider: .sqlite, output: '', // TODO: update output path ); diff --git a/pub/orm/lib/src/analyzer/fixes/_config_fix.dart b/pub/orm/lib/src/analyzer/fixes/_config_fix.dart index 15def782..eecf01b5 100644 --- a/pub/orm/lib/src/analyzer/fixes/_config_fix.dart +++ b/pub/orm/lib/src/analyzer/fixes/_config_fix.dart @@ -67,7 +67,7 @@ abstract class ConfigFix extends ResolvedCorrectionProducer { : '.sqlite'; return [ 'const config = ${qualifier}Config(', - '${utils.oneIndent}$provider,', + '${utils.oneIndent}provider: $provider,', "${utils.oneIndent}output: '', // TODO: update output path", ');', ].join(eol); From d82590f11161278eb414644d9d4b45a5eddc0f4c Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sun, 4 Jan 2026 23:14:29 +0800 Subject: [PATCH 08/15] Refactor config fix utilities into a mixin Extract common config helper methods from ConfigFix base class into ConfigUtils mixin Update ConfigRequiredFix and ConfigRequiredReplaceFix to use the mixin instead of inheritance Remove ConfigFix base class as it's no longer needed --- .../lib/src/analyzer/fixes/_config_fix.dart | 157 ------------------ .../lib/src/analyzer/fixes/_config_utils.dart | 81 +++++++++ .../analyzer/fixes/config_required_fix.dart | 44 ++++- .../fixes/config_required_replace_fix.dart | 51 +++++- .../analyzer/rules/config_required_rule.dart | 4 +- 5 files changed, 166 insertions(+), 171 deletions(-) delete mode 100644 pub/orm/lib/src/analyzer/fixes/_config_fix.dart create mode 100644 pub/orm/lib/src/analyzer/fixes/_config_utils.dart diff --git a/pub/orm/lib/src/analyzer/fixes/_config_fix.dart b/pub/orm/lib/src/analyzer/fixes/_config_fix.dart deleted file mode 100644 index eecf01b5..00000000 --- a/pub/orm/lib/src/analyzer/fixes/_config_fix.dart +++ /dev/null @@ -1,157 +0,0 @@ -import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; - -abstract class ConfigFix extends ResolvedCorrectionProducer { - ConfigFix({required super.context}); - - ImportDirective? findConfigImport(CompilationUnit unit) { - for (final directive in unit.directives) { - if (directive is! ImportDirective) { - continue; - } - final uri = directive.uri.stringValue; - if (uri == 'package:orm/config.dart') { - return directive; - } - } - return null; - } - - ConfigVariableInfo? findConfigVariable(CompilationUnit unit) { - for (final declaration in unit.declarations) { - if (declaration is! TopLevelVariableDeclaration) { - continue; - } - for (final variable in declaration.variables.variables) { - if (variable.name.lexeme == 'config') { - return ConfigVariableInfo(declaration, variable); - } - } - } - return null; - } - - int configInsertOffset(CompilationUnit unit) { - if (unit.directives.isEmpty) { - return 0; - } - return utils.getLineNext(unit.directives.last.end); - } - - int importInsertOffset(CompilationUnit unit) { - ImportDirective? lastImport; - LibraryDirective? libraryDirective; - for (final directive in unit.directives) { - if (directive is LibraryDirective) { - libraryDirective = directive; - } else if (directive is ImportDirective) { - lastImport = directive; - } - } - - final anchor = lastImport ?? libraryDirective; - if (anchor == null) { - return 0; - } - return utils.getLineNext(anchor.end); - } - - String buildImportText(String eol) => "import 'package:orm/config.dart';$eol"; - - String buildConfigText(String? prefix, String eol) { - final qualifier = (prefix == null || prefix.isEmpty) ? '' : '$prefix.'; - final provider = qualifier.isNotEmpty - ? '${qualifier}DatabaseProvider.sqlite' - : '.sqlite'; - return [ - 'const config = ${qualifier}Config(', - '${utils.oneIndent}provider: $provider,', - "${utils.oneIndent}output: '', // TODO: update output path", - ');', - ].join(eol); - } - - Future insertConfig( - ChangeBuilder builder, { - required int configDeclOffset, - }) async { - final eol = utils.endOfLine; - final configImport = findConfigImport(unit); - final prefix = configImport?.prefix?.name; - final needsImport = configImport == null; - - final importOffset = importInsertOffset(unit); - final configText = buildConfigText(prefix, eol); - final importText = buildImportText(eol); - - await builder.addDartFileEdit(file, (builder) { - if (needsImport && importOffset == configDeclOffset) { - builder.addInsertion(importOffset, (builder) { - builder.write(importText); - builder.write(eol); - builder.write(configText); - }); - return; - } - - if (needsImport) { - builder.addInsertion(importOffset, (builder) { - builder.write(importText); - }); - } - - builder.addInsertion(configDeclOffset, (builder) { - builder.write(configText); - }); - }); - } - - Future replaceConfig( - ChangeBuilder builder, { - required ConfigVariableInfo info, - }) async { - final eol = utils.endOfLine; - final configImport = findConfigImport(unit); - final prefix = configImport?.prefix?.name; - final needsImport = configImport == null; - - final importOffset = importInsertOffset(unit); - final configText = buildConfigText(prefix, eol); - final importText = buildImportText(eol); - - final declaration = info.declaration; - final replaceRange = SourceRange(declaration.offset, declaration.length); - - await builder.addDartFileEdit(file, (builder) { - if (needsImport && importOffset == replaceRange.offset) { - builder.addReplacement(replaceRange, (builder) { - builder.write(importText); - builder.write(eol); - builder.write(configText); - }); - return; - } - - if (needsImport) { - builder.addInsertion(importOffset, (builder) { - builder.write(importText); - }); - } - - builder.addReplacement(replaceRange, (builder) { - builder.write(configText); - }); - }); - } -} - -class ConfigVariableInfo { - final TopLevelVariableDeclaration declaration; - final VariableDeclaration variable; - - ConfigVariableInfo(this.declaration, this.variable); - - bool get isSingle => declaration.variables.variables.length == 1; -} diff --git a/pub/orm/lib/src/analyzer/fixes/_config_utils.dart b/pub/orm/lib/src/analyzer/fixes/_config_utils.dart new file mode 100644 index 00000000..f6b4a252 --- /dev/null +++ b/pub/orm/lib/src/analyzer/fixes/_config_utils.dart @@ -0,0 +1,81 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analyzer/dart/ast/ast.dart'; + +mixin ConfigUtils on ResolvedCorrectionProducer { + ImportDirective? findConfigImport(CompilationUnit unit) { + for (final directive in unit.directives) { + if (directive is! ImportDirective) { + continue; + } + final uri = directive.uri.stringValue; + if (uri == 'package:orm/config.dart') { + return directive; + } + } + return null; + } + + ConfigVariableInfo? findConfigVariable(CompilationUnit unit) { + for (final declaration in unit.declarations) { + if (declaration is! TopLevelVariableDeclaration) { + continue; + } + for (final variable in declaration.variables.variables) { + if (variable.name.lexeme == 'config') { + return .new(declaration, variable); + } + } + } + return null; + } + + int configInsertOffset(CompilationUnit unit) { + if (unit.directives.isEmpty) { + return 0; + } + return utils.getLineNext(unit.directives.last.end); + } + + int importInsertOffset(CompilationUnit unit) { + ImportDirective? lastImport; + LibraryDirective? libraryDirective; + for (final directive in unit.directives) { + if (directive is LibraryDirective) { + libraryDirective = directive; + } else if (directive is ImportDirective) { + lastImport = directive; + } + } + + final anchor = lastImport ?? libraryDirective; + if (anchor == null) { + return 0; + } + return utils.getLineNext(anchor.end); + } + + String buildImportText(String eol) => "import 'package:orm/config.dart';$eol"; + + String buildConfigText(String? prefix, String eol) { + final qualifier = (prefix == null || prefix.isEmpty) ? '' : '$prefix.'; + final provider = qualifier.isEmpty + ? '.sqlite' + : '${qualifier}DatabaseProvider.sqlite'; + return [ + 'const config = ${qualifier}Config(', + '${utils.oneIndent}provider: $provider,', + "${utils.oneIndent}output: '', // TODO: update output path", + ');', + '', + ].join(eol); + } +} + +class ConfigVariableInfo { + final TopLevelVariableDeclaration declaration; + final VariableDeclaration variable; + + ConfigVariableInfo(this.declaration, this.variable); + + bool get isSingle => declaration.variables.variables.length == 1; +} diff --git a/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart index c72c2f68..0dfc71c5 100644 --- a/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart +++ b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart @@ -3,10 +3,10 @@ import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; -import '_config_fix.dart'; +import '_config_utils.dart'; -class ConfigRequiredFix extends ConfigFix { - static const FixKind _kind = FixKind( +class ConfigRequiredFix extends ResolvedCorrectionProducer with ConfigUtils { + static const _kind = FixKind( 'orm.fix.config_required', DartFixKindPriority.standard, "Define ORM config: const config = Config(...)", @@ -15,8 +15,7 @@ class ConfigRequiredFix extends ConfigFix { ConfigRequiredFix({required super.context}); @override - CorrectionApplicability get applicability => - CorrectionApplicability.singleLocation; + CorrectionApplicability get applicability => .singleLocation; @override FixKind get fixKind => _kind; @@ -27,6 +26,39 @@ class ConfigRequiredFix extends ConfigFix { return; } - await insertConfig(builder, configDeclOffset: configInsertOffset(unit)); + await _insertConfig(builder); + } + + Future _insertConfig(ChangeBuilder builder) async { + final eol = utils.endOfLine; + final configImport = findConfigImport(unit); + final prefix = configImport?.prefix?.name; + final needsImport = configImport == null; + + final importOffset = importInsertOffset(unit); + final configDeclOffset = configInsertOffset(unit); + final configText = buildConfigText(prefix, eol); + final importText = buildImportText(eol); + + await builder.addDartFileEdit(file, (builder) { + if (needsImport && importOffset == configDeclOffset) { + builder.addInsertion(importOffset, (builder) { + builder.write(importText); + builder.write(eol); + builder.write(configText); + }); + return; + } + + if (needsImport) { + builder.addInsertion(importOffset, (builder) { + builder.write(importText); + }); + } + + builder.addInsertion(configDeclOffset, (builder) { + builder.write(configText); + }); + }); } } diff --git a/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart b/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart index 8b45134c..59465d41 100644 --- a/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart +++ b/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart @@ -1,12 +1,14 @@ import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; +import 'package:analyzer/source/source_range.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; -import '_config_fix.dart'; +import '_config_utils.dart'; -class ConfigRequiredReplaceFix extends ConfigFix { - static const FixKind _kind = FixKind( +class ConfigRequiredReplaceFix extends ResolvedCorrectionProducer + with ConfigUtils { + static const _kind = FixKind( 'orm.fix.config_required_replace', DartFixKindPriority.standard, "Replace ORM config: const config = Config(...)", @@ -15,8 +17,7 @@ class ConfigRequiredReplaceFix extends ConfigFix { ConfigRequiredReplaceFix({required super.context}); @override - CorrectionApplicability get applicability => - CorrectionApplicability.singleLocation; + CorrectionApplicability get applicability => .singleLocation; @override FixKind get fixKind => _kind; @@ -32,6 +33,44 @@ class ConfigRequiredReplaceFix extends ConfigFix { return; } - await replaceConfig(builder, info: info); + await _replaceConfig(builder, info: info); + } + + Future _replaceConfig( + ChangeBuilder builder, { + required ConfigVariableInfo info, + }) async { + final eol = utils.endOfLine; + final configImport = findConfigImport(unit); + final prefix = configImport?.prefix?.name; + final needsImport = configImport == null; + + final importOffset = importInsertOffset(unit); + final configText = buildConfigText(prefix, eol); + final importText = buildImportText(eol); + + final declaration = info.declaration; + final replaceRange = SourceRange(declaration.offset, declaration.length); + + await builder.addDartFileEdit(file, (builder) { + if (needsImport && importOffset == replaceRange.offset) { + builder.addReplacement(replaceRange, (builder) { + builder.write(importText); + builder.write(eol); + builder.write(configText); + }); + return; + } + + if (needsImport) { + builder.addInsertion(importOffset, (builder) { + builder.write(importText); + }); + } + + builder.addReplacement(replaceRange, (builder) { + builder.write(configText); + }); + }); } } diff --git a/pub/orm/lib/src/analyzer/rules/config_required_rule.dart b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart index 42b92ecb..605eefb5 100644 --- a/pub/orm/lib/src/analyzer/rules/config_required_rule.dart +++ b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart @@ -6,12 +6,12 @@ import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/error/error.dart'; class ConfigRequiredRule extends AnalysisRule { - static const LintCode code = LintCode( + static const code = LintCode( 'orm_config_required', "Missing required 'const config = Config(...);' in orm.config.dart.", correctionMessage: "Add a top-level 'const config = Config(...);' to orm.config.dart.", - severity: DiagnosticSeverity.ERROR, + severity: .ERROR, ); ConfigRequiredRule() From 1834bdac6e627f7d9b7bdf8509526c4e1909dcb1 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Sun, 4 Jan 2026 23:35:21 +0800 Subject: [PATCH 09/15] Refactor analyzer config utilities into shared modules --- pub/orm/lib/src/analyzer/README.md | 1 + .../lib/src/analyzer/fixes/_config_utils.dart | 81 ---------- .../analyzer/fixes/config_required_fix.dart | 47 +----- .../fixes/config_required_replace_fix.dart | 53 +------ .../analyzer/rules/config_required_rule.dart | 54 ++----- .../analyzer/utils/analyzer_constants.dart | 15 ++ .../src/analyzer/utils/config_ast_utils.dart | 33 ++++ .../lib/src/analyzer/utils/config_utils.dart | 150 ++++++++++++++++++ .../analyzer/config_required_fix_test.dart | 15 -- .../analyzer/config_required_rule_test.dart | 96 ----------- 10 files changed, 230 insertions(+), 315 deletions(-) delete mode 100644 pub/orm/lib/src/analyzer/fixes/_config_utils.dart create mode 100644 pub/orm/lib/src/analyzer/utils/analyzer_constants.dart create mode 100644 pub/orm/lib/src/analyzer/utils/config_ast_utils.dart create mode 100644 pub/orm/lib/src/analyzer/utils/config_utils.dart delete mode 100644 pub/orm/test/analyzer/config_required_fix_test.dart delete mode 100644 pub/orm/test/analyzer/config_required_rule_test.dart diff --git a/pub/orm/lib/src/analyzer/README.md b/pub/orm/lib/src/analyzer/README.md index e3ef1149..d920c28b 100644 --- a/pub/orm/lib/src/analyzer/README.md +++ b/pub/orm/lib/src/analyzer/README.md @@ -6,6 +6,7 @@ Structure - `rules/`: analysis rules (diagnostics). - `fixes/`: quick fixes for rule diagnostics. - `assists/`: assists not tied to diagnostics. +- `utils/`: shared helpers and constants. Naming - Rule file: `*_rule.dart` (class `*Rule`). diff --git a/pub/orm/lib/src/analyzer/fixes/_config_utils.dart b/pub/orm/lib/src/analyzer/fixes/_config_utils.dart deleted file mode 100644 index f6b4a252..00000000 --- a/pub/orm/lib/src/analyzer/fixes/_config_utils.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; -import 'package:analyzer/dart/ast/ast.dart'; - -mixin ConfigUtils on ResolvedCorrectionProducer { - ImportDirective? findConfigImport(CompilationUnit unit) { - for (final directive in unit.directives) { - if (directive is! ImportDirective) { - continue; - } - final uri = directive.uri.stringValue; - if (uri == 'package:orm/config.dart') { - return directive; - } - } - return null; - } - - ConfigVariableInfo? findConfigVariable(CompilationUnit unit) { - for (final declaration in unit.declarations) { - if (declaration is! TopLevelVariableDeclaration) { - continue; - } - for (final variable in declaration.variables.variables) { - if (variable.name.lexeme == 'config') { - return .new(declaration, variable); - } - } - } - return null; - } - - int configInsertOffset(CompilationUnit unit) { - if (unit.directives.isEmpty) { - return 0; - } - return utils.getLineNext(unit.directives.last.end); - } - - int importInsertOffset(CompilationUnit unit) { - ImportDirective? lastImport; - LibraryDirective? libraryDirective; - for (final directive in unit.directives) { - if (directive is LibraryDirective) { - libraryDirective = directive; - } else if (directive is ImportDirective) { - lastImport = directive; - } - } - - final anchor = lastImport ?? libraryDirective; - if (anchor == null) { - return 0; - } - return utils.getLineNext(anchor.end); - } - - String buildImportText(String eol) => "import 'package:orm/config.dart';$eol"; - - String buildConfigText(String? prefix, String eol) { - final qualifier = (prefix == null || prefix.isEmpty) ? '' : '$prefix.'; - final provider = qualifier.isEmpty - ? '.sqlite' - : '${qualifier}DatabaseProvider.sqlite'; - return [ - 'const config = ${qualifier}Config(', - '${utils.oneIndent}provider: $provider,', - "${utils.oneIndent}output: '', // TODO: update output path", - ');', - '', - ].join(eol); - } -} - -class ConfigVariableInfo { - final TopLevelVariableDeclaration declaration; - final VariableDeclaration variable; - - ConfigVariableInfo(this.declaration, this.variable); - - bool get isSingle => declaration.variables.variables.length == 1; -} diff --git a/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart index 0dfc71c5..f233d372 100644 --- a/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart +++ b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart @@ -3,19 +3,21 @@ import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; -import '_config_utils.dart'; +import '../utils/analyzer_constants.dart'; +import '../utils/config_utils.dart'; class ConfigRequiredFix extends ResolvedCorrectionProducer with ConfigUtils { - static const _kind = FixKind( - 'orm.fix.config_required', + static const FixKind _kind = FixKind( + configFixIdRequired, DartFixKindPriority.standard, - "Define ORM config: const config = Config(...)", + configFixMessageDefine, ); ConfigRequiredFix({required super.context}); @override - CorrectionApplicability get applicability => .singleLocation; + CorrectionApplicability get applicability => + CorrectionApplicability.singleLocation; @override FixKind get fixKind => _kind; @@ -26,39 +28,6 @@ class ConfigRequiredFix extends ResolvedCorrectionProducer with ConfigUtils { return; } - await _insertConfig(builder); - } - - Future _insertConfig(ChangeBuilder builder) async { - final eol = utils.endOfLine; - final configImport = findConfigImport(unit); - final prefix = configImport?.prefix?.name; - final needsImport = configImport == null; - - final importOffset = importInsertOffset(unit); - final configDeclOffset = configInsertOffset(unit); - final configText = buildConfigText(prefix, eol); - final importText = buildImportText(eol); - - await builder.addDartFileEdit(file, (builder) { - if (needsImport && importOffset == configDeclOffset) { - builder.addInsertion(importOffset, (builder) { - builder.write(importText); - builder.write(eol); - builder.write(configText); - }); - return; - } - - if (needsImport) { - builder.addInsertion(importOffset, (builder) { - builder.write(importText); - }); - } - - builder.addInsertion(configDeclOffset, (builder) { - builder.write(configText); - }); - }); + await insertConfig(builder, configDeclOffset: configInsertOffset(unit)); } } diff --git a/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart b/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart index 59465d41..73b3f69a 100644 --- a/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart +++ b/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart @@ -1,23 +1,24 @@ import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; -import 'package:analyzer/source/source_range.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; -import '_config_utils.dart'; +import '../utils/analyzer_constants.dart'; +import '../utils/config_utils.dart'; class ConfigRequiredReplaceFix extends ResolvedCorrectionProducer with ConfigUtils { - static const _kind = FixKind( - 'orm.fix.config_required_replace', + static const FixKind _kind = FixKind( + configFixIdRequiredReplace, DartFixKindPriority.standard, - "Replace ORM config: const config = Config(...)", + configFixMessageReplace, ); ConfigRequiredReplaceFix({required super.context}); @override - CorrectionApplicability get applicability => .singleLocation; + CorrectionApplicability get applicability => + CorrectionApplicability.singleLocation; @override FixKind get fixKind => _kind; @@ -33,44 +34,6 @@ class ConfigRequiredReplaceFix extends ResolvedCorrectionProducer return; } - await _replaceConfig(builder, info: info); - } - - Future _replaceConfig( - ChangeBuilder builder, { - required ConfigVariableInfo info, - }) async { - final eol = utils.endOfLine; - final configImport = findConfigImport(unit); - final prefix = configImport?.prefix?.name; - final needsImport = configImport == null; - - final importOffset = importInsertOffset(unit); - final configText = buildConfigText(prefix, eol); - final importText = buildImportText(eol); - - final declaration = info.declaration; - final replaceRange = SourceRange(declaration.offset, declaration.length); - - await builder.addDartFileEdit(file, (builder) { - if (needsImport && importOffset == replaceRange.offset) { - builder.addReplacement(replaceRange, (builder) { - builder.write(importText); - builder.write(eol); - builder.write(configText); - }); - return; - } - - if (needsImport) { - builder.addInsertion(importOffset, (builder) { - builder.write(importText); - }); - } - - builder.addReplacement(replaceRange, (builder) { - builder.write(configText); - }); - }); + await replaceConfig(builder, info: info); } } diff --git a/pub/orm/lib/src/analyzer/rules/config_required_rule.dart b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart index 605eefb5..e8547b1d 100644 --- a/pub/orm/lib/src/analyzer/rules/config_required_rule.dart +++ b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart @@ -5,18 +5,21 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/error/error.dart'; +import '../utils/analyzer_constants.dart'; +import '../utils/config_ast_utils.dart'; + class ConfigRequiredRule extends AnalysisRule { - static const code = LintCode( - 'orm_config_required', - "Missing required 'const config = Config(...);' in orm.config.dart.", + static const LintCode code = LintCode( + configRequiredRuleName, + "Missing required '$configFixSnippet' in $ormConfigFileName.", correctionMessage: - "Add a top-level 'const config = Config(...);' to orm.config.dart.", - severity: .ERROR, + "Add a top-level '$configFixSnippet' to $ormConfigFileName.", + severity: DiagnosticSeverity.ERROR, ); ConfigRequiredRule() : super( - name: 'orm_config_required', + name: configRequiredRuleName, description: 'Ensures orm.config.dart defines a top-level const Config.', ); @@ -47,7 +50,7 @@ class _Visitor extends SimpleAstVisitor { return; } - final configFile = packageRoot.getChildAssumingFile('orm.config.dart'); + final configFile = packageRoot.getChildAssumingFile(ormConfigFileName); if (currentUnit.file.path != configFile.path) { return; } @@ -58,8 +61,8 @@ class _Visitor extends SimpleAstVisitor { } bool _hasRequiredConfig(CompilationUnit unit) { - final configImport = _findConfigImport(unit); - final hasLocalConfig = _hasLocalConfigDeclaration(unit); + final configImport = findOrmConfigImport(unit); + final hasLocalConfig = hasLocalConfigDeclaration(unit); for (final declaration in unit.declarations) { if (declaration is! TopLevelVariableDeclaration) { continue; @@ -71,7 +74,7 @@ class _Visitor extends SimpleAstVisitor { } for (final variable in variables.variables) { - if (variable.name.lexeme != 'config') { + if (!isConfigVariable(variable)) { continue; } @@ -89,38 +92,12 @@ class _Visitor extends SimpleAstVisitor { return false; } - ImportDirective? _findConfigImport(CompilationUnit unit) { - for (final directive in unit.directives) { - if (directive is! ImportDirective) { - continue; - } - final uri = directive.uri.stringValue; - if (uri == 'package:orm/config.dart') { - return directive; - } - } - return null; - } - - bool _hasLocalConfigDeclaration(CompilationUnit unit) { - for (final declaration in unit.declarations) { - if (declaration is FunctionDeclaration) { - continue; - } - if (declaration is NamedCompilationUnitMember && - declaration.name.lexeme == 'Config') { - return true; - } - } - return false; - } - bool _isOrmConfigInitializer( InstanceCreationExpression initializer, ImportDirective? configImport, bool hasLocalConfig, ) { - if (initializer.constructorName.type.name.lexeme != 'Config') { + if (initializer.constructorName.type.name.lexeme != configClassName) { return false; } @@ -129,8 +106,7 @@ class _Visitor extends SimpleAstVisitor { final library = classElement?.library; final libraryUri = library?.firstFragment.source.uri; if (libraryUri != null) { - return libraryUri.scheme == 'package' && - libraryUri.path == 'orm/config.dart'; + return libraryUri.toString() == ormConfigImportUri; } if (configImport == null) { diff --git a/pub/orm/lib/src/analyzer/utils/analyzer_constants.dart b/pub/orm/lib/src/analyzer/utils/analyzer_constants.dart new file mode 100644 index 00000000..dad70576 --- /dev/null +++ b/pub/orm/lib/src/analyzer/utils/analyzer_constants.dart @@ -0,0 +1,15 @@ +const String ormConfigFileName = 'orm.config.dart'; +const String ormConfigImportUri = 'package:orm/config.dart'; +const String configClassName = 'Config'; +const String configVariableName = 'config'; + +const String configRequiredRuleName = 'orm_config_required'; + +const String configFixSnippet = + 'const $configVariableName = $configClassName(...)'; +const String configFixMessageDefine = 'Define ORM config: $configFixSnippet'; +const String configFixMessageReplace = + 'Replace ORM config: $configFixSnippet'; + +const String configFixIdRequired = 'orm.fix.config_required'; +const String configFixIdRequiredReplace = 'orm.fix.config_required_replace'; diff --git a/pub/orm/lib/src/analyzer/utils/config_ast_utils.dart b/pub/orm/lib/src/analyzer/utils/config_ast_utils.dart new file mode 100644 index 00000000..96060fe1 --- /dev/null +++ b/pub/orm/lib/src/analyzer/utils/config_ast_utils.dart @@ -0,0 +1,33 @@ +import 'package:analyzer/dart/ast/ast.dart'; + +import 'analyzer_constants.dart'; + +ImportDirective? findOrmConfigImport(CompilationUnit unit) { + for (final directive in unit.directives) { + if (directive is! ImportDirective) { + continue; + } + final uri = directive.uri.stringValue; + if (uri == ormConfigImportUri) { + return directive; + } + } + return null; +} + +bool hasLocalConfigDeclaration(CompilationUnit unit) { + for (final declaration in unit.declarations) { + if (declaration is FunctionDeclaration) { + continue; + } + if (declaration is NamedCompilationUnitMember && + declaration.name.lexeme == configClassName) { + return true; + } + } + return false; +} + +bool isConfigVariable(VariableDeclaration variable) { + return variable.name.lexeme == configVariableName; +} diff --git a/pub/orm/lib/src/analyzer/utils/config_utils.dart b/pub/orm/lib/src/analyzer/utils/config_utils.dart new file mode 100644 index 00000000..644fec88 --- /dev/null +++ b/pub/orm/lib/src/analyzer/utils/config_utils.dart @@ -0,0 +1,150 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; + +import 'analyzer_constants.dart'; +import 'config_ast_utils.dart'; + +mixin ConfigUtils on ResolvedCorrectionProducer { + ImportDirective? findConfigImport(CompilationUnit unit) { + return findOrmConfigImport(unit); + } + + ConfigVariableInfo? findConfigVariable(CompilationUnit unit) { + for (final declaration in unit.declarations) { + if (declaration is! TopLevelVariableDeclaration) { + continue; + } + for (final variable in declaration.variables.variables) { + if (isConfigVariable(variable)) { + return ConfigVariableInfo(declaration, variable); + } + } + } + return null; + } + + int configInsertOffset(CompilationUnit unit) { + if (unit.directives.isEmpty) { + return 0; + } + return utils.getLineNext(unit.directives.last.end); + } + + int importInsertOffset(CompilationUnit unit) { + ImportDirective? lastImport; + LibraryDirective? libraryDirective; + for (final directive in unit.directives) { + if (directive is LibraryDirective) { + libraryDirective = directive; + } else if (directive is ImportDirective) { + lastImport = directive; + } + } + + final anchor = lastImport ?? libraryDirective; + if (anchor == null) { + return 0; + } + return utils.getLineNext(anchor.end); + } + + String buildImportText(String eol) => "import '$ormConfigImportUri';$eol"; + + String buildConfigText(String? prefix, String eol) { + final qualifier = (prefix == null || prefix.isEmpty) ? '' : '$prefix.'; + final provider = qualifier.isEmpty + ? '.sqlite' + : '${qualifier}DatabaseProvider.sqlite'; + return [ + 'const $configVariableName = $qualifier$configClassName(', + '${utils.oneIndent}provider: $provider,', + "${utils.oneIndent}output: '', // TODO: update output path", + ');', + '', + ].join(eol); + } + + Future insertConfig( + ChangeBuilder builder, { + required int configDeclOffset, + }) async { + final eol = utils.endOfLine; + final configImport = findConfigImport(unit); + final prefix = configImport?.prefix?.name; + final needsImport = configImport == null; + + final importOffset = importInsertOffset(unit); + final configText = buildConfigText(prefix, eol); + final importText = buildImportText(eol); + + await builder.addDartFileEdit(file, (builder) { + if (needsImport && importOffset == configDeclOffset) { + builder.addInsertion(importOffset, (builder) { + builder.write(importText); + builder.write(eol); + builder.write(configText); + }); + return; + } + + if (needsImport) { + builder.addInsertion(importOffset, (builder) { + builder.write(importText); + }); + } + + builder.addInsertion(configDeclOffset, (builder) { + builder.write(configText); + }); + }); + } + + Future replaceConfig( + ChangeBuilder builder, { + required ConfigVariableInfo info, + }) async { + final eol = utils.endOfLine; + final configImport = findConfigImport(unit); + final prefix = configImport?.prefix?.name; + final needsImport = configImport == null; + + final importOffset = importInsertOffset(unit); + final configText = buildConfigText(prefix, eol); + final importText = buildImportText(eol); + + final declaration = info.declaration; + final replaceRange = SourceRange(declaration.offset, declaration.length); + + await builder.addDartFileEdit(file, (builder) { + if (needsImport && importOffset == replaceRange.offset) { + builder.addReplacement(replaceRange, (builder) { + builder.write(importText); + builder.write(eol); + builder.write(configText); + }); + return; + } + + if (needsImport) { + builder.addInsertion(importOffset, (builder) { + builder.write(importText); + }); + } + + builder.addReplacement(replaceRange, (builder) { + builder.write(configText); + }); + }); + } +} + +class ConfigVariableInfo { + final TopLevelVariableDeclaration declaration; + final VariableDeclaration variable; + + ConfigVariableInfo(this.declaration, this.variable); + + bool get isSingle => declaration.variables.variables.length == 1; +} diff --git a/pub/orm/test/analyzer/config_required_fix_test.dart b/pub/orm/test/analyzer/config_required_fix_test.dart deleted file mode 100644 index b604675e..00000000 --- a/pub/orm/test/analyzer/config_required_fix_test.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; -import 'package:orm/src/analyzer/fixes/config_required_fix.dart'; -import 'package:orm/src/analyzer/fixes/config_required_replace_fix.dart'; -import 'package:test/test.dart'; - -void main() { - test('fix kind ids', () { - final context = StubCorrectionProducerContext.instance; - final fix = ConfigRequiredFix(context: context); - final replaceFix = ConfigRequiredReplaceFix(context: context); - - expect(fix.fixKind.id, 'orm.fix.config_required'); - expect(replaceFix.fixKind.id, 'orm.fix.config_required_replace'); - }); -} diff --git a/pub/orm/test/analyzer/config_required_rule_test.dart b/pub/orm/test/analyzer/config_required_rule_test.dart deleted file mode 100644 index 2b5331c4..00000000 --- a/pub/orm/test/analyzer/config_required_rule_test.dart +++ /dev/null @@ -1,96 +0,0 @@ -// ignore_for_file: non_constant_identifier_names - -import 'package:analyzer/src/lint/registry.dart'; -import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; -import 'package:orm/src/analyzer/rules/config_required_rule.dart'; -import 'package:test_reflective_loader/test_reflective_loader.dart'; - -@reflectiveTest -class ConfigRequiredRuleTest extends AnalysisRuleTest { - @override - String get analysisRule => 'orm_config_required'; - - @override - void setUp() { - Registry.ruleRegistry.registerWarningRule(ConfigRequiredRule()); - super.setUp(); - newPubspecYamlFile(testPackageRootPath, 'name: orm\n'); - newFile(join(testPackageRootPath, 'lib', 'config.dart'), r''' -enum DatabaseProvider { sqlite } - -class Config { - final DatabaseProvider provider; - final String output; - - const Config({required this.provider, required this.output}); -} -'''); - } - - Future _assertMissingConfig(String content) async { - final path = join(testPackageRootPath, 'orm.config.dart'); - newFile(path, content); - await assertDiagnosticsInFile(path, [lint(0, content.length)]); - } - - Future _assertValidConfig(String content) async { - final path = join(testPackageRootPath, 'orm.config.dart'); - newFile(path, content); - await assertNoDiagnosticsInFile(path); - } - - void test_missingConfig() async { - await _assertMissingConfig(r''' -import 'package:orm/config.dart'; -'''); - } - - void test_validConfig() async { - await _assertValidConfig(r''' -import 'package:orm/config.dart'; - -const config = Config( - provider: DatabaseProvider.sqlite, - output: '', -); -'''); - } - - void test_prefixedImport() async { - await _assertValidConfig(r''' -import 'package:orm/config.dart' as orm; - -const config = orm.Config( - provider: orm.DatabaseProvider.sqlite, - output: '', -); -'''); - } - - void test_localConfigClass() async { - await _assertMissingConfig(r''' -class Config { - const Config(); -} - -const config = Config(); -'''); - } - - void test_notConst() async { - await _assertMissingConfig(r''' -import 'package:orm/config.dart'; - -final config = Config( - provider: DatabaseProvider.sqlite, - output: '', -); -'''); - } -} - -void main() { - defineReflectiveSuite(() { - defineReflectiveTests(ConfigRequiredRuleTest); - }); -} From 212a1f4fbaf6e2f6998c77513c84387479273725 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 5 Jan 2026 00:15:07 +0800 Subject: [PATCH 10/15] Remove analyzer constants and inline config-related strings --- pub/orm/lib/src/analyzer/README.md | 2 +- .../analyzer/fixes/config_required_fix.dart | 38 +++- .../fixes/config_required_replace_fix.dart | 42 +++- .../analyzer/rules/config_required_rule.dart | 19 +- .../analyzer/utils/analyzer_constants.dart | 15 -- .../src/analyzer/utils/config_ast_utils.dart | 33 --- .../lib/src/analyzer/utils/config_utils.dart | 199 +++++++----------- .../analyzer/config_required_fix_test.dart | 15 ++ .../analyzer/config_required_rule_test.dart | 96 +++++++++ 9 files changed, 268 insertions(+), 191 deletions(-) delete mode 100644 pub/orm/lib/src/analyzer/utils/analyzer_constants.dart delete mode 100644 pub/orm/lib/src/analyzer/utils/config_ast_utils.dart create mode 100644 pub/orm/test/analyzer/config_required_fix_test.dart create mode 100644 pub/orm/test/analyzer/config_required_rule_test.dart diff --git a/pub/orm/lib/src/analyzer/README.md b/pub/orm/lib/src/analyzer/README.md index d920c28b..d05f9bfe 100644 --- a/pub/orm/lib/src/analyzer/README.md +++ b/pub/orm/lib/src/analyzer/README.md @@ -6,7 +6,7 @@ Structure - `rules/`: analysis rules (diagnostics). - `fixes/`: quick fixes for rule diagnostics. - `assists/`: assists not tied to diagnostics. -- `utils/`: shared helpers and constants. +- `utils/`: shared helpers. Naming - Rule file: `*_rule.dart` (class `*Rule`). diff --git a/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart index f233d372..42b24598 100644 --- a/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart +++ b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart @@ -3,14 +3,13 @@ import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; -import '../utils/analyzer_constants.dart'; import '../utils/config_utils.dart'; -class ConfigRequiredFix extends ResolvedCorrectionProducer with ConfigUtils { +class ConfigRequiredFix extends ResolvedCorrectionProducer { static const FixKind _kind = FixKind( - configFixIdRequired, + 'orm.fix.config_required', DartFixKindPriority.standard, - configFixMessageDefine, + 'Define ORM config: const config = Config(...)', ); ConfigRequiredFix({required super.context}); @@ -28,6 +27,35 @@ class ConfigRequiredFix extends ResolvedCorrectionProducer with ConfigUtils { return; } - await insertConfig(builder, configDeclOffset: configInsertOffset(unit)); + final eol = utils.endOfLine; + final configImport = findConfigImport(unit); + final prefix = configImport?.prefix?.name; + final needsImport = configImport == null; + + final importOffset = importInsertOffset(unit); + final configDeclOffset = configInsertOffset(unit); + final configText = buildConfigText(prefix, eol, indent: utils.oneIndent); + final importText = buildImportText(eol); + + await builder.addDartFileEdit(file, (builder) { + if (needsImport && importOffset == configDeclOffset) { + builder.addInsertion(importOffset, (builder) { + builder.write(importText); + builder.write(eol); + builder.write(configText); + }); + return; + } + + if (needsImport) { + builder.addInsertion(importOffset, (builder) { + builder.write(importText); + }); + } + + builder.addInsertion(configDeclOffset, (builder) { + builder.write(configText); + }); + }); } } diff --git a/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart b/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart index 73b3f69a..4ae2c306 100644 --- a/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart +++ b/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart @@ -1,17 +1,16 @@ import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; +import 'package:analyzer/source/source_range.dart'; import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; -import '../utils/analyzer_constants.dart'; import '../utils/config_utils.dart'; -class ConfigRequiredReplaceFix extends ResolvedCorrectionProducer - with ConfigUtils { +class ConfigRequiredReplaceFix extends ResolvedCorrectionProducer { static const FixKind _kind = FixKind( - configFixIdRequiredReplace, + 'orm.fix.config_required_replace', DartFixKindPriority.standard, - configFixMessageReplace, + 'Replace ORM config: const config = Config(...)', ); ConfigRequiredReplaceFix({required super.context}); @@ -34,6 +33,37 @@ class ConfigRequiredReplaceFix extends ResolvedCorrectionProducer return; } - await replaceConfig(builder, info: info); + final eol = utils.endOfLine; + final configImport = findConfigImport(unit); + final prefix = configImport?.prefix?.name; + final needsImport = configImport == null; + + final importOffset = importInsertOffset(unit); + final configText = buildConfigText(prefix, eol, indent: utils.oneIndent); + final importText = buildImportText(eol); + + final declaration = info.declaration; + final replaceRange = SourceRange(declaration.offset, declaration.length); + + await builder.addDartFileEdit(file, (builder) { + if (needsImport && importOffset == replaceRange.offset) { + builder.addReplacement(replaceRange, (builder) { + builder.write(importText); + builder.write(eol); + builder.write(configText); + }); + return; + } + + if (needsImport) { + builder.addInsertion(importOffset, (builder) { + builder.write(importText); + }); + } + + builder.addReplacement(replaceRange, (builder) { + builder.write(configText); + }); + }); } } diff --git a/pub/orm/lib/src/analyzer/rules/config_required_rule.dart b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart index e8547b1d..6131d8ca 100644 --- a/pub/orm/lib/src/analyzer/rules/config_required_rule.dart +++ b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart @@ -5,21 +5,20 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/error/error.dart'; -import '../utils/analyzer_constants.dart'; -import '../utils/config_ast_utils.dart'; +import '../utils/config_utils.dart'; class ConfigRequiredRule extends AnalysisRule { static const LintCode code = LintCode( - configRequiredRuleName, - "Missing required '$configFixSnippet' in $ormConfigFileName.", + 'orm_config_required', + "Missing required 'const config = Config(...)' in orm.config.dart.", correctionMessage: - "Add a top-level '$configFixSnippet' to $ormConfigFileName.", + "Add a top-level 'const config = Config(...)' to orm.config.dart.", severity: DiagnosticSeverity.ERROR, ); ConfigRequiredRule() : super( - name: configRequiredRuleName, + name: 'orm_config_required', description: 'Ensures orm.config.dart defines a top-level const Config.', ); @@ -50,7 +49,7 @@ class _Visitor extends SimpleAstVisitor { return; } - final configFile = packageRoot.getChildAssumingFile(ormConfigFileName); + final configFile = packageRoot.getChildAssumingFile('orm.config.dart'); if (currentUnit.file.path != configFile.path) { return; } @@ -61,7 +60,7 @@ class _Visitor extends SimpleAstVisitor { } bool _hasRequiredConfig(CompilationUnit unit) { - final configImport = findOrmConfigImport(unit); + final configImport = findConfigImport(unit); final hasLocalConfig = hasLocalConfigDeclaration(unit); for (final declaration in unit.declarations) { if (declaration is! TopLevelVariableDeclaration) { @@ -97,7 +96,7 @@ class _Visitor extends SimpleAstVisitor { ImportDirective? configImport, bool hasLocalConfig, ) { - if (initializer.constructorName.type.name.lexeme != configClassName) { + if (initializer.constructorName.type.name.lexeme != 'Config') { return false; } @@ -106,7 +105,7 @@ class _Visitor extends SimpleAstVisitor { final library = classElement?.library; final libraryUri = library?.firstFragment.source.uri; if (libraryUri != null) { - return libraryUri.toString() == ormConfigImportUri; + return libraryUri.toString() == 'package:orm/config.dart'; } if (configImport == null) { diff --git a/pub/orm/lib/src/analyzer/utils/analyzer_constants.dart b/pub/orm/lib/src/analyzer/utils/analyzer_constants.dart deleted file mode 100644 index dad70576..00000000 --- a/pub/orm/lib/src/analyzer/utils/analyzer_constants.dart +++ /dev/null @@ -1,15 +0,0 @@ -const String ormConfigFileName = 'orm.config.dart'; -const String ormConfigImportUri = 'package:orm/config.dart'; -const String configClassName = 'Config'; -const String configVariableName = 'config'; - -const String configRequiredRuleName = 'orm_config_required'; - -const String configFixSnippet = - 'const $configVariableName = $configClassName(...)'; -const String configFixMessageDefine = 'Define ORM config: $configFixSnippet'; -const String configFixMessageReplace = - 'Replace ORM config: $configFixSnippet'; - -const String configFixIdRequired = 'orm.fix.config_required'; -const String configFixIdRequiredReplace = 'orm.fix.config_required_replace'; diff --git a/pub/orm/lib/src/analyzer/utils/config_ast_utils.dart b/pub/orm/lib/src/analyzer/utils/config_ast_utils.dart deleted file mode 100644 index 96060fe1..00000000 --- a/pub/orm/lib/src/analyzer/utils/config_ast_utils.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:analyzer/dart/ast/ast.dart'; - -import 'analyzer_constants.dart'; - -ImportDirective? findOrmConfigImport(CompilationUnit unit) { - for (final directive in unit.directives) { - if (directive is! ImportDirective) { - continue; - } - final uri = directive.uri.stringValue; - if (uri == ormConfigImportUri) { - return directive; - } - } - return null; -} - -bool hasLocalConfigDeclaration(CompilationUnit unit) { - for (final declaration in unit.declarations) { - if (declaration is FunctionDeclaration) { - continue; - } - if (declaration is NamedCompilationUnitMember && - declaration.name.lexeme == configClassName) { - return true; - } - } - return false; -} - -bool isConfigVariable(VariableDeclaration variable) { - return variable.name.lexeme == configVariableName; -} diff --git a/pub/orm/lib/src/analyzer/utils/config_utils.dart b/pub/orm/lib/src/analyzer/utils/config_utils.dart index 644fec88..b3b3bcaf 100644 --- a/pub/orm/lib/src/analyzer/utils/config_utils.dart +++ b/pub/orm/lib/src/analyzer/utils/config_utils.dart @@ -1,143 +1,100 @@ -import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; -import 'analyzer_constants.dart'; -import 'config_ast_utils.dart'; - -mixin ConfigUtils on ResolvedCorrectionProducer { - ImportDirective? findConfigImport(CompilationUnit unit) { - return findOrmConfigImport(unit); - } - - ConfigVariableInfo? findConfigVariable(CompilationUnit unit) { - for (final declaration in unit.declarations) { - if (declaration is! TopLevelVariableDeclaration) { - continue; - } - for (final variable in declaration.variables.variables) { - if (isConfigVariable(variable)) { - return ConfigVariableInfo(declaration, variable); - } - } +ImportDirective? findConfigImport(CompilationUnit unit) { + for (final directive in unit.directives) { + if (directive is! ImportDirective) { + continue; } - return null; - } - - int configInsertOffset(CompilationUnit unit) { - if (unit.directives.isEmpty) { - return 0; + final uri = directive.uri.stringValue; + if (uri == 'package:orm/config.dart') { + return directive; } - return utils.getLineNext(unit.directives.last.end); } + return null; +} - int importInsertOffset(CompilationUnit unit) { - ImportDirective? lastImport; - LibraryDirective? libraryDirective; - for (final directive in unit.directives) { - if (directive is LibraryDirective) { - libraryDirective = directive; - } else if (directive is ImportDirective) { - lastImport = directive; - } +bool hasLocalConfigDeclaration(CompilationUnit unit) { + for (final declaration in unit.declarations) { + if (declaration is FunctionDeclaration) { + continue; } - - final anchor = lastImport ?? libraryDirective; - if (anchor == null) { - return 0; + if (declaration is NamedCompilationUnitMember && + declaration.name.lexeme == 'Config') { + return true; } - return utils.getLineNext(anchor.end); - } - - String buildImportText(String eol) => "import '$ormConfigImportUri';$eol"; - - String buildConfigText(String? prefix, String eol) { - final qualifier = (prefix == null || prefix.isEmpty) ? '' : '$prefix.'; - final provider = qualifier.isEmpty - ? '.sqlite' - : '${qualifier}DatabaseProvider.sqlite'; - return [ - 'const $configVariableName = $qualifier$configClassName(', - '${utils.oneIndent}provider: $provider,', - "${utils.oneIndent}output: '', // TODO: update output path", - ');', - '', - ].join(eol); } + return false; +} - Future insertConfig( - ChangeBuilder builder, { - required int configDeclOffset, - }) async { - final eol = utils.endOfLine; - final configImport = findConfigImport(unit); - final prefix = configImport?.prefix?.name; - final needsImport = configImport == null; - - final importOffset = importInsertOffset(unit); - final configText = buildConfigText(prefix, eol); - final importText = buildImportText(eol); - - await builder.addDartFileEdit(file, (builder) { - if (needsImport && importOffset == configDeclOffset) { - builder.addInsertion(importOffset, (builder) { - builder.write(importText); - builder.write(eol); - builder.write(configText); - }); - return; - } +bool isConfigVariable(VariableDeclaration variable) { + return variable.name.lexeme == 'config'; +} - if (needsImport) { - builder.addInsertion(importOffset, (builder) { - builder.write(importText); - }); +ConfigVariableInfo? findConfigVariable(CompilationUnit unit) { + for (final declaration in unit.declarations) { + if (declaration is! TopLevelVariableDeclaration) { + continue; + } + for (final variable in declaration.variables.variables) { + if (isConfigVariable(variable)) { + return ConfigVariableInfo(declaration, variable); } - - builder.addInsertion(configDeclOffset, (builder) { - builder.write(configText); - }); - }); + } } + return null; +} - Future replaceConfig( - ChangeBuilder builder, { - required ConfigVariableInfo info, - }) async { - final eol = utils.endOfLine; - final configImport = findConfigImport(unit); - final prefix = configImport?.prefix?.name; - final needsImport = configImport == null; - - final importOffset = importInsertOffset(unit); - final configText = buildConfigText(prefix, eol); - final importText = buildImportText(eol); +int configInsertOffset(CompilationUnit unit) { + if (unit.directives.isEmpty) { + return 0; + } + return _nextLineOffset(unit, unit.directives.last.end); +} - final declaration = info.declaration; - final replaceRange = SourceRange(declaration.offset, declaration.length); +int importInsertOffset(CompilationUnit unit) { + ImportDirective? lastImport; + LibraryDirective? libraryDirective; + for (final directive in unit.directives) { + if (directive is LibraryDirective) { + libraryDirective = directive; + } else if (directive is ImportDirective) { + lastImport = directive; + } + } - await builder.addDartFileEdit(file, (builder) { - if (needsImport && importOffset == replaceRange.offset) { - builder.addReplacement(replaceRange, (builder) { - builder.write(importText); - builder.write(eol); - builder.write(configText); - }); - return; - } + final anchor = lastImport ?? libraryDirective; + if (anchor == null) { + return 0; + } + return _nextLineOffset(unit, anchor.end); +} - if (needsImport) { - builder.addInsertion(importOffset, (builder) { - builder.write(importText); - }); - } +String buildImportText(String eol) => "import 'package:orm/config.dart';$eol"; + +String buildConfigText( + String? prefix, + String eol, { + required String indent, +}) { + final qualifier = (prefix == null || prefix.isEmpty) ? '' : '$prefix.'; + final provider = + qualifier.isEmpty ? '.sqlite' : '${qualifier}DatabaseProvider.sqlite'; + return [ + 'const config = $qualifier' 'Config(', + '${indent}provider: $provider,', + "${indent}output: '', // TODO: update output path", + ');', + '', + ].join(eol); +} - builder.addReplacement(replaceRange, (builder) { - builder.write(configText); - }); - }); +int _nextLineOffset(CompilationUnit unit, int offset) { + final lineInfo = unit.lineInfo; + final lineNumber = lineInfo.getLocation(offset).lineNumber; + if (lineNumber >= lineInfo.lineCount) { + return unit.end; } + return lineInfo.getOffsetOfLine(lineNumber); } class ConfigVariableInfo { diff --git a/pub/orm/test/analyzer/config_required_fix_test.dart b/pub/orm/test/analyzer/config_required_fix_test.dart new file mode 100644 index 00000000..b604675e --- /dev/null +++ b/pub/orm/test/analyzer/config_required_fix_test.dart @@ -0,0 +1,15 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:orm/src/analyzer/fixes/config_required_fix.dart'; +import 'package:orm/src/analyzer/fixes/config_required_replace_fix.dart'; +import 'package:test/test.dart'; + +void main() { + test('fix kind ids', () { + final context = StubCorrectionProducerContext.instance; + final fix = ConfigRequiredFix(context: context); + final replaceFix = ConfigRequiredReplaceFix(context: context); + + expect(fix.fixKind.id, 'orm.fix.config_required'); + expect(replaceFix.fixKind.id, 'orm.fix.config_required_replace'); + }); +} diff --git a/pub/orm/test/analyzer/config_required_rule_test.dart b/pub/orm/test/analyzer/config_required_rule_test.dart new file mode 100644 index 00000000..2b5331c4 --- /dev/null +++ b/pub/orm/test/analyzer/config_required_rule_test.dart @@ -0,0 +1,96 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:analyzer/src/lint/registry.dart'; +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:orm/src/analyzer/rules/config_required_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +@reflectiveTest +class ConfigRequiredRuleTest extends AnalysisRuleTest { + @override + String get analysisRule => 'orm_config_required'; + + @override + void setUp() { + Registry.ruleRegistry.registerWarningRule(ConfigRequiredRule()); + super.setUp(); + newPubspecYamlFile(testPackageRootPath, 'name: orm\n'); + newFile(join(testPackageRootPath, 'lib', 'config.dart'), r''' +enum DatabaseProvider { sqlite } + +class Config { + final DatabaseProvider provider; + final String output; + + const Config({required this.provider, required this.output}); +} +'''); + } + + Future _assertMissingConfig(String content) async { + final path = join(testPackageRootPath, 'orm.config.dart'); + newFile(path, content); + await assertDiagnosticsInFile(path, [lint(0, content.length)]); + } + + Future _assertValidConfig(String content) async { + final path = join(testPackageRootPath, 'orm.config.dart'); + newFile(path, content); + await assertNoDiagnosticsInFile(path); + } + + void test_missingConfig() async { + await _assertMissingConfig(r''' +import 'package:orm/config.dart'; +'''); + } + + void test_validConfig() async { + await _assertValidConfig(r''' +import 'package:orm/config.dart'; + +const config = Config( + provider: DatabaseProvider.sqlite, + output: '', +); +'''); + } + + void test_prefixedImport() async { + await _assertValidConfig(r''' +import 'package:orm/config.dart' as orm; + +const config = orm.Config( + provider: orm.DatabaseProvider.sqlite, + output: '', +); +'''); + } + + void test_localConfigClass() async { + await _assertMissingConfig(r''' +class Config { + const Config(); +} + +const config = Config(); +'''); + } + + void test_notConst() async { + await _assertMissingConfig(r''' +import 'package:orm/config.dart'; + +final config = Config( + provider: DatabaseProvider.sqlite, + output: '', +); +'''); + } +} + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(ConfigRequiredRuleTest); + }); +} From 95982e5e88de2d07c3a94c6a814022a2d6749878 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 5 Jan 2026 00:31:10 +0800 Subject: [PATCH 11/15] Remove unused import and add config file in test setup --- pub/orm/test/analyzer/config_required_rule_test.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pub/orm/test/analyzer/config_required_rule_test.dart b/pub/orm/test/analyzer/config_required_rule_test.dart index 2b5331c4..b7f725e7 100644 --- a/pub/orm/test/analyzer/config_required_rule_test.dart +++ b/pub/orm/test/analyzer/config_required_rule_test.dart @@ -1,20 +1,20 @@ // ignore_for_file: non_constant_identifier_names -import 'package:analyzer/src/lint/registry.dart'; import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:orm/src/analyzer/rules/config_required_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; @reflectiveTest class ConfigRequiredRuleTest extends AnalysisRuleTest { - @override - String get analysisRule => 'orm_config_required'; - @override void setUp() { - Registry.ruleRegistry.registerWarningRule(ConfigRequiredRule()); + rule = ConfigRequiredRule(); super.setUp(); newPubspecYamlFile(testPackageRootPath, 'name: orm\n'); + newSinglePackageConfigJsonFile( + packagePath: testPackageRootPath, + name: 'orm', + ); newFile(join(testPackageRootPath, 'lib', 'config.dart'), r''' enum DatabaseProvider { sqlite } @@ -41,6 +41,7 @@ class Config { void test_missingConfig() async { await _assertMissingConfig(r''' +// ignore_for_file: unused_import import 'package:orm/config.dart'; '''); } From 5e682afa42f9361cf9dd7d0123e830aab4aa2f67 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 5 Jan 2026 01:30:16 +0800 Subject: [PATCH 12/15] Remove config replace fix and improve diagnostic node selection --- pub/orm/lib/main.dart | 5 -- .../fixes/config_required_replace_fix.dart | 69 ------------------- .../analyzer/rules/config_required_rule.dart | 16 ++++- .../analyzer/config_required_fix_test.dart | 3 - .../analyzer/config_required_rule_test.dart | 22 +++++- 5 files changed, 36 insertions(+), 79 deletions(-) delete mode 100644 pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart diff --git a/pub/orm/lib/main.dart b/pub/orm/lib/main.dart index 415eb44e..c63aed11 100644 --- a/pub/orm/lib/main.dart +++ b/pub/orm/lib/main.dart @@ -2,7 +2,6 @@ import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/registry.dart'; import 'src/analyzer/fixes/config_required_fix.dart'; -import 'src/analyzer/fixes/config_required_replace_fix.dart'; import 'src/analyzer/rules/config_required_rule.dart'; class AnalysisPlugin extends Plugin { @@ -13,10 +12,6 @@ class AnalysisPlugin extends Plugin { void register(PluginRegistry registry) { registry.registerWarningRule(ConfigRequiredRule()); registry.registerFixForRule(ConfigRequiredRule.code, ConfigRequiredFix.new); - registry.registerFixForRule( - ConfigRequiredRule.code, - ConfigRequiredReplaceFix.new, - ); } } diff --git a/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart b/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart deleted file mode 100644 index 4ae2c306..00000000 --- a/pub/orm/lib/src/analyzer/fixes/config_required_replace_fix.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; -import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; -import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; - -import '../utils/config_utils.dart'; - -class ConfigRequiredReplaceFix extends ResolvedCorrectionProducer { - static const FixKind _kind = FixKind( - 'orm.fix.config_required_replace', - DartFixKindPriority.standard, - 'Replace ORM config: const config = Config(...)', - ); - - ConfigRequiredReplaceFix({required super.context}); - - @override - CorrectionApplicability get applicability => - CorrectionApplicability.singleLocation; - - @override - FixKind get fixKind => _kind; - - @override - Future compute(ChangeBuilder builder) async { - final info = findConfigVariable(unit); - if (info == null) { - return; - } - - if (!info.isSingle || info.declaration.metadata.isNotEmpty) { - return; - } - - final eol = utils.endOfLine; - final configImport = findConfigImport(unit); - final prefix = configImport?.prefix?.name; - final needsImport = configImport == null; - - final importOffset = importInsertOffset(unit); - final configText = buildConfigText(prefix, eol, indent: utils.oneIndent); - final importText = buildImportText(eol); - - final declaration = info.declaration; - final replaceRange = SourceRange(declaration.offset, declaration.length); - - await builder.addDartFileEdit(file, (builder) { - if (needsImport && importOffset == replaceRange.offset) { - builder.addReplacement(replaceRange, (builder) { - builder.write(importText); - builder.write(eol); - builder.write(configText); - }); - return; - } - - if (needsImport) { - builder.addInsertion(importOffset, (builder) { - builder.write(importText); - }); - } - - builder.addReplacement(replaceRange, (builder) { - builder.write(configText); - }); - }); - } -} diff --git a/pub/orm/lib/src/analyzer/rules/config_required_rule.dart b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart index 6131d8ca..753eaabf 100644 --- a/pub/orm/lib/src/analyzer/rules/config_required_rule.dart +++ b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart @@ -55,10 +55,24 @@ class _Visitor extends SimpleAstVisitor { } if (!_hasRequiredConfig(node)) { - rule.reportAtNode(node); + rule.reportAtNode(_diagnosticNode(node)); } } + AstNode _diagnosticNode(CompilationUnit unit) { + final configInfo = findConfigVariable(unit); + if (configInfo != null) { + return configInfo.variable; + } + if (unit.declarations.isNotEmpty) { + return unit.declarations.first; + } + if (unit.directives.isNotEmpty) { + return unit.directives.last; + } + return unit; + } + bool _hasRequiredConfig(CompilationUnit unit) { final configImport = findConfigImport(unit); final hasLocalConfig = hasLocalConfigDeclaration(unit); diff --git a/pub/orm/test/analyzer/config_required_fix_test.dart b/pub/orm/test/analyzer/config_required_fix_test.dart index b604675e..9e658b68 100644 --- a/pub/orm/test/analyzer/config_required_fix_test.dart +++ b/pub/orm/test/analyzer/config_required_fix_test.dart @@ -1,15 +1,12 @@ import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; import 'package:orm/src/analyzer/fixes/config_required_fix.dart'; -import 'package:orm/src/analyzer/fixes/config_required_replace_fix.dart'; import 'package:test/test.dart'; void main() { test('fix kind ids', () { final context = StubCorrectionProducerContext.instance; final fix = ConfigRequiredFix(context: context); - final replaceFix = ConfigRequiredReplaceFix(context: context); expect(fix.fixKind.id, 'orm.fix.config_required'); - expect(replaceFix.fixKind.id, 'orm.fix.config_required_replace'); }); } diff --git a/pub/orm/test/analyzer/config_required_rule_test.dart b/pub/orm/test/analyzer/config_required_rule_test.dart index b7f725e7..6aad30c8 100644 --- a/pub/orm/test/analyzer/config_required_rule_test.dart +++ b/pub/orm/test/analyzer/config_required_rule_test.dart @@ -1,7 +1,9 @@ // ignore_for_file: non_constant_identifier_names import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/dart/ast/ast.dart'; import 'package:orm/src/analyzer/rules/config_required_rule.dart'; +import 'package:orm/src/analyzer/utils/config_utils.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; @reflectiveTest @@ -30,7 +32,11 @@ class Config { Future _assertMissingConfig(String content) async { final path = join(testPackageRootPath, 'orm.config.dart'); newFile(path, content); - await assertDiagnosticsInFile(path, [lint(0, content.length)]); + final resolved = await resolveFile(path); + final diagnosticNode = _diagnosticNode(resolved.unit); + await assertDiagnosticsInFile(path, [ + lint(diagnosticNode.offset, diagnosticNode.length), + ]); } Future _assertValidConfig(String content) async { @@ -90,6 +96,20 @@ final config = Config( } } +AstNode _diagnosticNode(CompilationUnit unit) { + final configInfo = findConfigVariable(unit); + if (configInfo != null) { + return configInfo.variable; + } + if (unit.declarations.isNotEmpty) { + return unit.declarations.first; + } + if (unit.directives.isNotEmpty) { + return unit.directives.last; + } + return unit; +} + void main() { defineReflectiveSuite(() { defineReflectiveTests(ConfigRequiredRuleTest); From 6f544ce4a9f30e4c63ad6a93b5277a64f993690c Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 5 Jan 2026 01:32:57 +0800 Subject: [PATCH 13/15] Update test methods to return Future --- pub/orm/test/analyzer/config_required_rule_test.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pub/orm/test/analyzer/config_required_rule_test.dart b/pub/orm/test/analyzer/config_required_rule_test.dart index 6aad30c8..8cd8479e 100644 --- a/pub/orm/test/analyzer/config_required_rule_test.dart +++ b/pub/orm/test/analyzer/config_required_rule_test.dart @@ -45,14 +45,14 @@ class Config { await assertNoDiagnosticsInFile(path); } - void test_missingConfig() async { + Future test_missingConfig() async { await _assertMissingConfig(r''' // ignore_for_file: unused_import import 'package:orm/config.dart'; '''); } - void test_validConfig() async { + Future test_validConfig() async { await _assertValidConfig(r''' import 'package:orm/config.dart'; @@ -63,7 +63,7 @@ const config = Config( '''); } - void test_prefixedImport() async { + Future test_prefixedImport() async { await _assertValidConfig(r''' import 'package:orm/config.dart' as orm; @@ -74,7 +74,7 @@ const config = orm.Config( '''); } - void test_localConfigClass() async { + Future test_localConfigClass() async { await _assertMissingConfig(r''' class Config { const Config(); @@ -84,7 +84,7 @@ const config = Config(); '''); } - void test_notConst() async { + Future test_notConst() async { await _assertMissingConfig(r''' import 'package:orm/config.dart'; From d02e476f8fc9ef7418a5ef4a1b9d65999f452020 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 5 Jan 2026 01:44:07 +0800 Subject: [PATCH 14/15] Change `references` and `fields` from Iterable to Set --- playground/orm.schema.dart | 14 ++++++++++++++ pub/orm/lib/src/schema/relation.dart | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 playground/orm.schema.dart diff --git a/playground/orm.schema.dart b/playground/orm.schema.dart new file mode 100644 index 00000000..464bc31a --- /dev/null +++ b/playground/orm.schema.dart @@ -0,0 +1,14 @@ +import 'package:orm/schema.dart'; + +@model +typedef User = ({@id String id, String email}); + +@model +typedef Post = ({ + @id String id, + String title, + String content, + String authorId, + + @Relation(references: {'authorId'}) User author, +}); diff --git a/pub/orm/lib/src/schema/relation.dart b/pub/orm/lib/src/schema/relation.dart index 360a33d2..50cb7f76 100644 --- a/pub/orm/lib/src/schema/relation.dart +++ b/pub/orm/lib/src/schema/relation.dart @@ -29,10 +29,10 @@ final class Relation { final String? map; /// A list of fields of the model on the other side of the relation - final Iterable references; + final Set references; /// A list of fields of the current model - final Iterable? fields; + final Set? fields; /// Defines the referential action to perform when a referenced /// entry in the referenced model is being deleted. From 9ff460334cdb943a2ff6dae8e1bf5d7509e2af04 Mon Sep 17 00:00:00 2001 From: Seven Du <5564821+medz@users.noreply.github.com> Date: Mon, 5 Jan 2026 01:50:06 +0800 Subject: [PATCH 15/15] Change Relation annotation fields and references parameters --- playground/orm.schema.dart | 2 +- pub/orm/lib/src/schema/relation.dart | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/playground/orm.schema.dart b/playground/orm.schema.dart index 464bc31a..6d3a6850 100644 --- a/playground/orm.schema.dart +++ b/playground/orm.schema.dart @@ -10,5 +10,5 @@ typedef Post = ({ String content, String authorId, - @Relation(references: {'authorId'}) User author, + @Relation(fields: {'authorId'}) User author, }); diff --git a/pub/orm/lib/src/schema/relation.dart b/pub/orm/lib/src/schema/relation.dart index 50cb7f76..c25ea403 100644 --- a/pub/orm/lib/src/schema/relation.dart +++ b/pub/orm/lib/src/schema/relation.dart @@ -29,10 +29,10 @@ final class Relation { final String? map; /// A list of fields of the model on the other side of the relation - final Set references; + final Set? references; /// A list of fields of the current model - final Set? fields; + final Set fields; /// Defines the referential action to perform when a referenced /// entry in the referenced model is being deleted. @@ -45,10 +45,10 @@ final class Relation { @literal /// Creates a relation annotation describing a model relationship. const Relation({ - required this.references, + required this.fields, + this.references, this.name, this.map, - this.fields, this.onDelete, this.onUpdate, });