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..6ffb183a --- /dev/null +++ b/playground/orm.config.dart @@ -0,0 +1,6 @@ +import 'package:orm/config.dart'; + +const config = Config( + provider: .sqlite, + output: '', // TODO: update output path +); diff --git a/playground/orm.schema.dart b/playground/orm.schema.dart new file mode 100644 index 00000000..6d3a6850 --- /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(fields: {'authorId'}) User author, +}); 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/main.dart b/pub/orm/lib/main.dart index 4079f807..c63aed11 100644 --- a/pub/orm/lib/main.dart +++ b/pub/orm/lib/main.dart @@ -1,22 +1,18 @@ -// import 'dart:async'; +import 'package:analysis_server_plugin/plugin.dart'; +import 'package:analysis_server_plugin/registry.dart'; -// import 'package:analysis_server_plugin/registry.dart'; +import 'src/analyzer/fixes/config_required_fix.dart'; +import 'src/analyzer/rules/config_required_rule.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 + void register(PluginRegistry registry) { + registry.registerWarningRule(ConfigRequiredRule()); + registry.registerFixForRule(ConfigRequiredRule.code, ConfigRequiredFix.new); + } +} -// // 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(); 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/analyzer/README.md b/pub/orm/lib/src/analyzer/README.md new file mode 100644 index 00000000..d05f9bfe --- /dev/null +++ b/pub/orm/lib/src/analyzer/README.md @@ -0,0 +1,26 @@ +# 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. +- `utils/`: shared helpers. + +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/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..42b24598 --- /dev/null +++ b/pub/orm/lib/src/analyzer/fixes/config_required_fix.dart @@ -0,0 +1,61 @@ +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 '../utils/config_utils.dart'; + +class ConfigRequiredFix extends ResolvedCorrectionProducer { + static const FixKind _kind = FixKind( + 'orm.fix.config_required', + DartFixKindPriority.standard, + 'Define ORM config: const config = Config(...)', + ); + + ConfigRequiredFix({required super.context}); + + @override + CorrectionApplicability get applicability => + CorrectionApplicability.singleLocation; + + @override + FixKind get fixKind => _kind; + + @override + Future compute(ChangeBuilder builder) async { + if (findConfigVariable(unit) != null) { + return; + } + + 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/rules/config_required_rule.dart b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart new file mode 100644 index 00000000..753eaabf --- /dev/null +++ b/pub/orm/lib/src/analyzer/rules/config_required_rule.dart @@ -0,0 +1,143 @@ +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'; + +import '../utils/config_utils.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.", + severity: DiagnosticSeverity.ERROR, + ); + + 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(_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); + 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 (!isConfigVariable(variable)) { + continue; + } + + final initializer = variable.initializer; + if (initializer is InstanceCreationExpression && + _isOrmConfigInitializer( + initializer, + configImport, + hasLocalConfig, + )) { + 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.toString() == 'package: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; + } +} 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..b3b3bcaf --- /dev/null +++ b/pub/orm/lib/src/analyzer/utils/config_utils.dart @@ -0,0 +1,107 @@ +import 'package:analyzer/dart/ast/ast.dart'; + +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 isConfigVariable(VariableDeclaration variable) { + return variable.name.lexeme == 'config'; +} + +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 _nextLineOffset(unit, 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 _nextLineOffset(unit, anchor.end); +} + +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); +} + +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 { + 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/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); } diff --git a/pub/orm/lib/src/schema/relation.dart b/pub/orm/lib/src/schema/relation.dart index 360a33d2..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 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. @@ -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, }); 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 @@ - diff --git a/pub/orm/pubspec.yaml b/pub/orm/pubspec.yaml index ab09b508..59cfa70c 100644 --- a/pub/orm/pubspec.yaml +++ b/pub/orm/pubspec.yaml @@ -11,7 +11,10 @@ dependencies: analyzer: ^9.0.0 analysis_server_plugin: ^0.3.4 meta: ^1.17.0 + 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..9e658b68 --- /dev/null +++ b/pub/orm/test/analyzer/config_required_fix_test.dart @@ -0,0 +1,12 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:orm/src/analyzer/fixes/config_required_fix.dart'; +import 'package:test/test.dart'; + +void main() { + test('fix kind ids', () { + final context = StubCorrectionProducerContext.instance; + final fix = ConfigRequiredFix(context: context); + + expect(fix.fixKind.id, 'orm.fix.config_required'); + }); +} 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..8cd8479e --- /dev/null +++ b/pub/orm/test/analyzer/config_required_rule_test.dart @@ -0,0 +1,117 @@ +// 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 +class ConfigRequiredRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = ConfigRequiredRule(); + super.setUp(); + newPubspecYamlFile(testPackageRootPath, 'name: orm\n'); + newSinglePackageConfigJsonFile( + packagePath: testPackageRootPath, + name: 'orm', + ); + 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); + final resolved = await resolveFile(path); + final diagnosticNode = _diagnosticNode(resolved.unit); + await assertDiagnosticsInFile(path, [ + lint(diagnosticNode.offset, diagnosticNode.length), + ]); + } + + Future _assertValidConfig(String content) async { + final path = join(testPackageRootPath, 'orm.config.dart'); + newFile(path, content); + await assertNoDiagnosticsInFile(path); + } + + Future test_missingConfig() async { + await _assertMissingConfig(r''' +// ignore_for_file: unused_import +import 'package:orm/config.dart'; +'''); + } + + Future test_validConfig() async { + await _assertValidConfig(r''' +import 'package:orm/config.dart'; + +const config = Config( + provider: DatabaseProvider.sqlite, + output: '', +); +'''); + } + + Future test_prefixedImport() async { + await _assertValidConfig(r''' +import 'package:orm/config.dart' as orm; + +const config = orm.Config( + provider: orm.DatabaseProvider.sqlite, + output: '', +); +'''); + } + + Future test_localConfigClass() async { + await _assertMissingConfig(r''' +class Config { + const Config(); +} + +const config = Config(); +'''); + } + + Future test_notConst() async { + await _assertMissingConfig(r''' +import 'package:orm/config.dart'; + +final config = Config( + provider: DatabaseProvider.sqlite, + output: '', +); +'''); + } +} + +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); + }); +} 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: