diff --git a/lib/main.dart b/lib/main.dart index 010dcaff..e758297e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/registry.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; @@ -23,21 +24,24 @@ class SolidLintsPlugin extends Plugin { @override void register(PluginRegistry registry) { - registry.registerLintRule( + final analysisLoader = AnalysisOptionsLoader(); + + final doubleLiteralFormatRule = DoubleLiteralFormatRule(); + final lintRules = [ AvoidGlobalStateRule(), - ); - registry.registerLintRule( AvoidNonNullAssertionRule(), - ); - registry.registerLintRule( AvoidDebugPrintInReleaseRule(), - ); - registry.registerLintRule( + doubleLiteralFormatRule, ProperSuperCallsRule(), - ); + // TODO: Add more lint rules and use analysisLoader + // for rules that need parameters + // For example: `CyclomaticComplexityRule(analysisLoader)` + ]; + + for (final lintRule in lintRules) { + registry.registerLintRule(lintRule); + } - final doubleLiteralFormatRule = DoubleLiteralFormatRule(); - registry.registerLintRule(doubleLiteralFormatRule); for (final code in doubleLiteralFormatRule.diagnosticCodes) { registry.registerFixForRule(code, DoubleLiteralFormatFix.new); } diff --git a/lib/src/common/parameter_parser/analysis_options_loader.dart b/lib/src/common/parameter_parser/analysis_options_loader.dart new file mode 100644 index 00000000..ab22ecc9 --- /dev/null +++ b/lib/src/common/parameter_parser/analysis_options_loader.dart @@ -0,0 +1,109 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:solid_lints/src/common/parameter_parser/cached_package_rules.dart'; +import 'package:yaml/yaml.dart'; + +/// Loads and parses analysis options from a Dart project's YAML file. +class AnalysisOptionsLoader { + final ResourceProvider _resourceProvider; + final Map _rulesCache = {}; + + /// Creates an instance of [AnalysisOptionsLoader] + AnalysisOptionsLoader({ResourceProvider? resourceProvider}) + : _resourceProvider = + resourceProvider ?? PhysicalResourceProvider.INSTANCE; + + /// Gets the options for a specific rule by its name. + Map? getRuleOptions(RuleContext context, String ruleName) => + _withNearestAnalysisOptionsFilePathForContext?>( + context, + (path) => _rulesCache[path]?.rules[ruleName], + ); + + /// Loads lint rules from the analysis options file for all rules + /// using the provided [RuleContext]. + void loadRulesOptionsFromContext(RuleContext context) => + _withNearestAnalysisOptionsFilePathForContext( + context, + _loadRulesOptionsIfNewer, + ); + + T? _withNearestAnalysisOptionsFilePathForContext( + RuleContext context, + T Function(String) f, + ) { + final packageRootPath = context.package?.root.path; + if (packageRootPath == null) return null; + + final yamlPath = _findNearestAnalysisOptionsFilePath(packageRootPath); + if (yamlPath == null) return null; + + return f(yamlPath); + } + + void _loadRulesOptionsIfNewer(String yamlPath) { + final analysisOptionsFile = _resourceProvider.getFile(yamlPath); + final modificationStamp = analysisOptionsFile.modificationStamp; + final cachedRules = _rulesCache[yamlPath]; + + if (cachedRules?.modificationStamp == modificationStamp) { + return; + } + + final rules = _getRules(analysisOptionsFile); + _rulesCache[yamlPath] = CachedPackageRules( + modificationStamp: modificationStamp, + rules: rules, + ); + } + + String? _findNearestAnalysisOptionsFilePath(String packageRootPath) { + final pathContext = _resourceProvider.pathContext; + String currentDirectoryPath = packageRootPath; + + while (pathContext.dirname(currentDirectoryPath) != currentDirectoryPath) { + final candidatePath = + pathContext.join(currentDirectoryPath, 'analysis_options.yaml'); + final candidateFile = _resourceProvider.getFile(candidatePath); + + if (candidateFile.exists) { + return candidatePath; + } + + final parentDir = pathContext.dirname(currentDirectoryPath); + currentDirectoryPath = parentDir; + } + + return null; + } + + Map> _getRules(File? analysisOptionsFile) { + if (analysisOptionsFile == null || !analysisOptionsFile.exists) { + return {}; + } + + final optionsString = analysisOptionsFile.readAsStringSync(); + Object? yaml; + try { + yaml = loadYaml(optionsString) as Object?; + } catch (err) { + return {}; + } + + if (yaml + case {'plugins': {'solid_lints': {'diagnostics': final diagnostics?}}} + when diagnostics is Map) { + return Map.fromEntries( + diagnostics.entries.where((e) => e.key is String && e.value is Map).map( + (e) => MapEntry( + e.key as String, + Map.from(e.value as Map), + ), + ), + ); + } + + return {}; + } +} diff --git a/lib/src/common/parameter_parser/cached_package_rules.dart b/lib/src/common/parameter_parser/cached_package_rules.dart new file mode 100644 index 00000000..a65201d9 --- /dev/null +++ b/lib/src/common/parameter_parser/cached_package_rules.dart @@ -0,0 +1,14 @@ +/// Cached rules for a dart package +class CachedPackageRules { + /// The last modification stamp of the analysis options file + final int modificationStamp; + + /// Cached rules options by rule name for the package + final Map> rules; + + /// Creates an instance of [CachedPackageRules] + const CachedPackageRules({ + required this.modificationStamp, + required this.rules, + }); +} diff --git a/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart b/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart index 5110bbf7..0b288a0d 100644 --- a/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart +++ b/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart @@ -1,10 +1,9 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; +import 'package:solid_lints/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; -import 'package:solid_lints/src/utils/types_utils.dart'; /// Avoid `late` keyword /// @@ -48,62 +47,37 @@ import 'package:solid_lints/src/utils/types_utils.dart'; /// } /// ``` class AvoidLateKeywordRule extends SolidLintRule { - /// This lint rule represents - /// the error whether we use `late` keyword. - static const lintName = 'avoid_late_keyword'; + static const String _lintName = 'avoid_late_keyword'; - AvoidLateKeywordRule._(super.config); + static const LintCode _code = LintCode( + _lintName, + 'Avoid using the "late" keyword. It may result in runtime exceptions.', + ); - /// Creates a new instance of [AvoidLateKeywordRule] - /// based on the lint configuration. - factory AvoidLateKeywordRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - paramsParser: AvoidLateKeywordParameters.fromJson, - problemMessage: (_) => 'Avoid using the "late" keyword. ' - 'It may result in runtime exceptions.', - ); + /// Creates an instance of [AvoidLateKeywordRule]. + AvoidLateKeywordRule({required super.analysisOptionsLoader}) + : super.withParameters( + name: _lintName, + description: 'Warns against using the late keyword.', + parametersParser: AvoidLateKeywordParameters.fromJson, + ); - return AvoidLateKeywordRule._(rule); - } + @override + LintCode get diagnosticCode => _code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addVariableDeclaration((node) { - if (_shouldLint(node)) { - reporter.atNode(node, code); - } - }); - } - - bool _shouldLint(VariableDeclaration node) { - final isLateDeclaration = node.isLate; - if (!isLateDeclaration) return false; - - final hasIgnoredType = _hasIgnoredType(node); - if (hasIgnoredType) return false; - - final allowInitialized = config.parameters.allowInitialized; - if (!allowInitialized) return true; - - final hasInitializer = node.initializer != null; - return !hasInitializer; - } - - bool _hasIgnoredType(VariableDeclaration node) { - final ignoredTypes = config.parameters.ignoredTypes.toSet(); - if (ignoredTypes.isEmpty) return false; + final parameters = + getParametersForContext(context) ?? const AvoidLateKeywordParameters(); - final variableType = node.declaredFragment?.element.type; - if (variableType == null) return false; + final visitor = AvoidLateKeywordVisitor(this, parameters); - return variableType.hasIgnoredType( - ignoredTypes: ignoredTypes, + registry.addVariableDeclaration( + this, + visitor, ); } } diff --git a/lib/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart b/lib/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart new file mode 100644 index 00000000..fc1fa960 --- /dev/null +++ b/lib/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart @@ -0,0 +1,48 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart'; +import 'package:solid_lints/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; + +/// Visitor for [AvoidLateKeywordRule]. +class AvoidLateKeywordVisitor extends SimpleAstVisitor { + final AvoidLateKeywordRule _rule; + + final AvoidLateKeywordParameters _parameters; + + /// Creates an instance of [AvoidLateKeywordVisitor]. + AvoidLateKeywordVisitor(this._rule, this._parameters); + + @override + void visitVariableDeclaration(VariableDeclaration node) { + if (!_shouldReport(node)) return; + + _rule.reportAtNode(node); + } + + bool _shouldReport(VariableDeclaration node) { + final isLateDeclaration = node.isLate; + if (!isLateDeclaration) return false; + + final hasIgnoredType = _hasIgnoredType(node); + if (hasIgnoredType) return false; + + final allowInitialized = _parameters.allowInitialized; + if (!allowInitialized) return true; + + final hasInitializer = node.initializer != null; + return !hasInitializer; + } + + bool _hasIgnoredType(VariableDeclaration node) { + final ignoredTypes = _parameters.ignoredTypes.toSet(); + if (ignoredTypes.isEmpty) return false; + + final variableType = node.declaredFragment?.element.type; + if (variableType == null) return false; + + return variableType.hasIgnoredType( + ignoredTypes: ignoredTypes, + ); + } +} diff --git a/lib/src/models/rule_config.dart b/lib/src/models/rule_config.dart deleted file mode 100644 index 35c35b96..00000000 --- a/lib/src/models/rule_config.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:analyzer/error/error.dart' as error; -import 'package:custom_lint_builder/custom_lint_builder.dart'; - -/// Type definition of a value factory which allows us to map data from -/// YAML configuration to an object of type [T]. -typedef RuleParameterParser = T Function(Map json); - -/// Type definition for a problem message factory after finding a problem -/// by a given lint. -typedef RuleProblemFactory = String Function(T value); - -/// [RuleConfig] allows us to quickly parse a lint rule and -/// declare basic configuration for it. -class RuleConfig { - /// Constructor for [RuleConfig] model. - RuleConfig({ - required this.name, - required CustomLintConfigs configs, - required RuleProblemFactory problemMessage, - RuleParameterParser? paramsParser, - }) : enabled = configs.rules[name]?.enabled ?? false, - parameters = paramsParser?.call(configs.rules[name]?.json ?? {}) as T, - _problemMessageFactory = problemMessage; - - /// This lint rule represents the error. - final String name; - - /// A flag which indicates whether this rule was enabled by the user. - final bool enabled; - - /// Value with a configuration for a particular rule. - final T parameters; - - /// Factory for generating error messages. - final RuleProblemFactory _problemMessageFactory; - - /// [LintCode] which is generated based on the provided data. - LintCode get lintCode => LintCode( - name: name, - problemMessage: _problemMessageFactory(parameters), - errorSeverity: error.DiagnosticSeverity.WARNING, - ); -} diff --git a/lib/src/models/solid_lint_rule.dart b/lib/src/models/solid_lint_rule.dart index a536eb4f..9f5bee56 100644 --- a/lib/src/models/solid_lint_rule.dart +++ b/lib/src/models/solid_lint_rule.dart @@ -1,16 +1,43 @@ -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; + +/// A function that parses the rule parameters from analysis options json +typedef RuleParametersParser = T Function(Map); /// A base class for emitting information about /// issues with user's `.dart` files. -abstract class SolidLintRule extends DartLintRule { +abstract class SolidLintRule extends AnalysisRule { + final AnalysisOptionsLoader? _analysisOptionsLoader; + + final RuleParametersParser? _parametersParser; + /// Constructor for [SolidLintRule] model. - SolidLintRule(this.config) : super(code: config.lintCode); + SolidLintRule({ + required super.name, + required super.description, + super.state, + }) : _analysisOptionsLoader = null, + _parametersParser = null; + + /// Constructor for [SolidLintRule] model with parameters. + SolidLintRule.withParameters({ + required AnalysisOptionsLoader analysisOptionsLoader, + required RuleParametersParser parametersParser, + required super.name, + required super.description, + super.state, + }) : _analysisOptionsLoader = analysisOptionsLoader, + _parametersParser = parametersParser; + + /// Reads the rule parameters from analysis options and parses them to [T] + T? getParametersForContext(RuleContext context) { + _analysisOptionsLoader?.loadRulesOptionsFromContext(context); - /// Configuration for a particular rule with all the - /// defined custom parameters. - final RuleConfig config; + final unparsedParameters = + _analysisOptionsLoader?.getRuleOptions(context, name); + if (unparsedParameters == null) return null; - /// A flag which indicates whether this rule was enabled by the user. - bool get enabled => config.enabled; + return _parametersParser?.call(unparsedParameters); + } } diff --git a/lint_test/avoid_late_keyword/allow_initialized/analysis_options.yaml b/lint_test/avoid_late_keyword/allow_initialized/analysis_options.yaml deleted file mode 100644 index 7f92efe8..00000000 --- a/lint_test/avoid_late_keyword/allow_initialized/analysis_options.yaml +++ /dev/null @@ -1,10 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - avoid_late_keyword: - allow_initialized: false - ignored_types: - - Animation diff --git a/lint_test/avoid_late_keyword/allow_initialized/avoid_late_keyword_allow_initialized_test.dart b/lint_test/avoid_late_keyword/allow_initialized/avoid_late_keyword_allow_initialized_test.dart deleted file mode 100644 index 05af24d1..00000000 --- a/lint_test/avoid_late_keyword/allow_initialized/avoid_late_keyword_allow_initialized_test.dart +++ /dev/null @@ -1,84 +0,0 @@ -// ignore_for_file: prefer_const_declarations, unused_local_variable, prefer_match_file_name -// ignore_for_file: avoid_global_state - -abstract class Animation {} - -class AnimationController implements Animation {} - -class SubAnimationController extends AnimationController {} - -class ColorTween {} - -/// Check "late" keyword fail -/// -/// `avoid_late_keyword` -/// allow_initialized option disabled -class AvoidLateKeyword { - /// ignored_types: Animation - late final Animation animation1; - - /// ignored_types: Animation - late final animation2 = AnimationController(); - - /// ignored_types: Animation - late final animation3 = SubAnimationController(); - - /// expect_lint: avoid_late_keyword - late final ColorTween colorTween1; - - /// expect_lint: avoid_late_keyword - late final colorTween2 = ColorTween(); - - /// expect_lint: avoid_late_keyword - late final colorTween3 = colorTween2; - - /// ignored_types: Animation - late final AnimationController controller1; - - /// expect_lint: avoid_late_keyword - late final field1 = 'string'; - - /// expect_lint: avoid_late_keyword - late final String field2; - - /// expect_lint: avoid_late_keyword - late final String field3 = 'string'; - - /// expect_lint: avoid_late_keyword - late final field4; - - void test() { - /// ignored_types: Animation - late final Animation animation1; - - /// ignored_types: Animation - late final animation2 = AnimationController(); - - /// ignored_types: Animation - late final animation3 = SubAnimationController(); - - /// expect_lint: avoid_late_keyword - late final ColorTween colorTween1; - - /// expect_lint: avoid_late_keyword - late final colorTween2 = ColorTween(); - - /// expect_lint: avoid_late_keyword - late final colorTween3 = colorTween2; - - /// ignored_types: Animation - late final AnimationController controller1; - - /// expect_lint: avoid_late_keyword - late final local1 = 'string'; - - /// expect_lint: avoid_late_keyword - late final String local2; - - /// expect_lint: avoid_late_keyword - late final String local4 = 'string'; - - /// expect_lint: avoid_late_keyword - late final local3; - } -} diff --git a/lint_test/avoid_late_keyword/no_generics/analysis_options.yaml b/lint_test/avoid_late_keyword/no_generics/analysis_options.yaml deleted file mode 100644 index da6add2f..00000000 --- a/lint_test/avoid_late_keyword/no_generics/analysis_options.yaml +++ /dev/null @@ -1,10 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - avoid_late_keyword: - allow_initialized: false - ignored_types: - - Subscription diff --git a/lint_test/avoid_late_keyword/no_generics/avoid_late_keyword_no_generics_test.dart b/lint_test/avoid_late_keyword/no_generics/avoid_late_keyword_no_generics_test.dart deleted file mode 100644 index 7c166749..00000000 --- a/lint_test/avoid_late_keyword/no_generics/avoid_late_keyword_no_generics_test.dart +++ /dev/null @@ -1,52 +0,0 @@ -// ignore_for_file: prefer_const_declarations, unused_local_variable, prefer_match_file_name -// ignore_for_file: avoid_global_state - -class Subscription {} - -class ConcreteTypeWithNoGenerics {} - -class NotAllowed {} - -/// Check "late" keyword fail -/// -/// `avoid_late_keyword` -/// allow_initialized option disabled -class AvoidLateKeyword { - /// expect_lint: avoid_late_keyword - late final NotAllowed na1; - - /// ignored_types: Subscription - late final Subscription subscription1; - - /// ignored_types: Subscription - late final Subscription subscription2; - - /// ignored_types: Subscription - late final Subscription> subscription3; - - /// ignored_types: Subscription - late final Subscription>> subscription4; - - /// ignored_types: Subscription - late final Subscription> subscription5; - - void test() { - /// expect_lint: avoid_late_keyword - late final NotAllowed na1; - - /// ignored_types: Subscription - late final Subscription subscription1; - - /// ignored_types: Subscription - late final Subscription subscription2; - - /// ignored_types: Subscription - late final Subscription> subscription3; - - /// ignored_types: Subscription - late final Subscription>> subscription4; - - /// ignored_types: Subscription - late final Subscription> subscription5; - } -} diff --git a/lint_test/avoid_late_keyword/with_generics/analysis_options.yaml b/lint_test/avoid_late_keyword/with_generics/analysis_options.yaml deleted file mode 100644 index 47219688..00000000 --- a/lint_test/avoid_late_keyword/with_generics/analysis_options.yaml +++ /dev/null @@ -1,14 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - avoid_late_keyword: - allow_initialized: true - ignored_types: - - ColorTween - - AnimationController - - Subscription> - - Subscription> - - Subscription diff --git a/lint_test/avoid_late_keyword/with_generics/avoid_late_keyword_with_generics_test.dart b/lint_test/avoid_late_keyword/with_generics/avoid_late_keyword_with_generics_test.dart deleted file mode 100644 index 9778438e..00000000 --- a/lint_test/avoid_late_keyword/with_generics/avoid_late_keyword_with_generics_test.dart +++ /dev/null @@ -1,114 +0,0 @@ -// ignore_for_file: prefer_const_declarations, unused_local_variable, prefer_match_file_name -// ignore_for_file: avoid_global_state - -class ColorTween {} - -class AnimationController {} - -class SubAnimationController extends AnimationController {} - -class Allowed {} - -class NotAllowed {} - -class Subscription {} - -class ConcreteTypeWithNoGenerics {} - -/// Check "late" keyword fail -/// -/// `avoid_late_keyword` -/// allow_initialized option enabled -class AvoidLateKeyword { - /// ignored_types: ColorTween - late final ColorTween colorTween; - - /// ignored_types: AnimationController - late final AnimationController controller1; - - /// ignored_types: AnimationController - late final SubAnimationController controller2; - - /// ignored_types: AnimationController - late final controller3 = AnimationController(); - - /// ignored_types: AnimationController - late final controller4 = SubAnimationController(); - - /// allow_initialized: true - late final field1 = 'string'; - - /// expect_lint: avoid_late_keyword - late final String field2; - - /// expect_lint: avoid_late_keyword - late final field3; - - /// expect_lint: avoid_late_keyword - late final NotAllowed na1; - - /// allow_initialized: true - late final a = Allowed(); - - /// expect_lint: avoid_late_keyword - late final Subscription subscription1; - - /// ignored_types: Subscription - late final Subscription subscription2; - - /// ignored_types: Subscription> - late final Subscription> subscription3; - - /// ignored_types: Subscription> - late final Subscription>> subscription4; - - /// ignored_types: Subscription> - late final Subscription> subscription5; - - /// ignored_types: Subscription> - late final Subscription> subscription6; - - /// expect_lint: avoid_late_keyword - late final Subscription> subscription7; - - void test() { - /// ignored_types: ColorTween - late final ColorTween colorTween; - - /// ignored_types: AnimationController - late final AnimationController controller1; - - /// ignored_types: AnimationController - late final SubAnimationController controller2; - - /// ignored_types: AnimationController - late final controller3 = AnimationController(); - - /// ignored_types: AnimationController - late final controller4 = SubAnimationController(); - - /// allow_initialized: true - late final local1 = 'string'; - - /// expect_lint: avoid_late_keyword - late final String local2; - - /// expect_lint: avoid_late_keyword - late final local3; - - /// expect_lint: avoid_late_keyword - late final NotAllowed na1; - - /// allow_initialized: true - late final a = Allowed(); - - /// expect_lint: avoid_late_keyword - late final Subscription subscription1; - - /// ignored_types: Subscription - late final Subscription subscription2; - - /// ignored_types: Subscription> - late final Subscription> subscription3; - } -} diff --git a/test/avoid_late_keyword_rule_test.dart b/test/avoid_late_keyword_rule_test.dart new file mode 100644 index 00000000..778f9f9d --- /dev/null +++ b/test/avoid_late_keyword_rule_test.dart @@ -0,0 +1,350 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidLateKeywordRuleTest); + defineReflectiveTests(AvoidLateKeywordNoGenericsTest); + defineReflectiveTests(AvoidLateKeywordWithGenericsTest); + }); +} + +@reflectiveTest +class AvoidLateKeywordRuleTest extends AnalysisRuleTest { + final String _typesDefinitions = ''' +abstract class Animation {} + +class AnimationController implements Animation {} + +class SubAnimationController extends AnimationController {} + +class ColorTween {} +'''; + + @override + void setUp() { + rule = AvoidLateKeywordRule( + analysisOptionsLoader: + AnalysisOptionsLoader(resourceProvider: resourceProvider), + ); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + ${rule.name}: + allow_initialized: false + ignored_types: + - Animation +''', + ); + } + + Future test_does_not_report_ignored_types_fields() async { + await assertNoDiagnostics( + ''' +class Test { + late final Animation animation1; + late final animation2 = AnimationController(); + late final animation3 = SubAnimationController(); + late final AnimationController controller1; +} +$_typesDefinitions + ''', + ); + } + + Future test_does_not_report_ignored_types_local_variables() async { + await assertNoDiagnostics( + ''' +void test() { + late final Animation animation1; + late final animation2 = AnimationController(); + late final animation3 = SubAnimationController(); + late final AnimationController controller1; +} +$_typesDefinitions + ''', + ); + } + + Future test_reports_non_ignored_types_fields() async { + await assertDiagnostics( + ''' +class Test { + late final ColorTween colorTween1; + late final colorTween2 = ColorTween(); + late final colorTween3 = colorTween2; + late final field1 = 'string'; + late final String field2; + late final String field3 = 'string'; + late final field4; +} +$_typesDefinitions + ''', + [ + lint(37, 11), + lint(63, 26), + lint(104, 25), + lint(144, 17), + lint(183, 6), + lint(211, 17), + lint(243, 6), + ], + ); + } + + Future test_reports_non_ignored_types_local_variables() async { + await assertDiagnostics( + ''' +void test() { + late final ColorTween colorTween1; + late final colorTween2 = ColorTween(); + late final colorTween3 = colorTween2; + late final local1 = 'string'; + late final String local2; + late final String local4 = 'string'; + late final local3; +} +$_typesDefinitions + ''', + [ + lint(38, 11), + lint(64, 26), + lint(105, 25), + lint(145, 17), + lint(184, 6), + lint(212, 17), + lint(244, 6), + ], + ); + } +} + +@reflectiveTest +class AvoidLateKeywordNoGenericsTest extends AnalysisRuleTest { + final String _typesDefinitions = ''' +class Subscription {} + +class ConcreteTypeWithNoGenerics {} + +class NotAllowed {} +'''; + + @override + void setUp() { + rule = AvoidLateKeywordRule( + analysisOptionsLoader: + AnalysisOptionsLoader(resourceProvider: resourceProvider), + ); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + ${rule.name}: + allow_initialized: false + ignored_types: + - Subscription +''', + ); + } + + Future test_does_not_report_ignored_types_fields() async { + await assertNoDiagnostics( + ''' +class Test { + late final Subscription subscription1; + late final Subscription subscription2; + late final Subscription> subscription3; + late final Subscription>> subscription4; + late final Subscription> subscription5; +} +$_typesDefinitions + ''', + ); + } + + Future test_does_not_report_ignored_types_local_variables() async { + await assertNoDiagnostics( + ''' +void test() { + late final Subscription subscription1; + late final Subscription subscription2; + late final Subscription> subscription3; + late final Subscription>> subscription4; + late final Subscription> subscription5; +} +$_typesDefinitions + ''', + ); + } + + Future test_reports_non_ignored_types_fields() async { + await assertDiagnostics( + ''' +class Test { + late final NotAllowed na1; +} +$_typesDefinitions + ''', + [ + lint(37, 3), + ], + ); + } + + Future test_reports_non_ignored_types_local_variables() async { + await assertDiagnostics( + ''' +void test() { + late final NotAllowed na1; +} +$_typesDefinitions + ''', + [ + lint(38, 3), + ], + ); + } +} + +@reflectiveTest +class AvoidLateKeywordWithGenericsTest extends AnalysisRuleTest { + final String _typesDefinitions = ''' +class ColorTween {} + +class AnimationController {} + +class SubAnimationController extends AnimationController {} + +class Allowed {} + +class NotAllowed {} + +class Subscription {} + +class ConcreteTypeWithNoGenerics {} +'''; + + @override + void setUp() { + rule = AvoidLateKeywordRule( + analysisOptionsLoader: + AnalysisOptionsLoader(resourceProvider: resourceProvider), + ); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + ${rule.name}: + allow_initialized: true + ignored_types: + - ColorTween + - AnimationController + - Subscription> + - Subscription> + - Subscription +''', + ); + } + + Future test_does_not_report_ignored_types_fields() async { + await assertNoDiagnostics( + ''' +class Test { + late final ColorTween colorTween; + late final AnimationController controller1; + late final SubAnimationController controller2; + late final controller3 = AnimationController(); + late final controller4 = SubAnimationController(); + late final Subscription subscription2; + late final Subscription> subscription3; + late final Subscription>> subscription4; + late final Subscription> subscription5; + late final Subscription> subscription6; + late final field1 = 'string'; + late final a = Allowed(); +} +$_typesDefinitions + ''', + ); + } + + Future test_does_not_report_ignored_types_local_variables() async { + await assertNoDiagnostics( + ''' +void test() { + late final ColorTween colorTween; + late final AnimationController controller1; + late final SubAnimationController controller2; + late final controller3 = AnimationController(); + late final controller4 = SubAnimationController(); + late final Subscription subscription2; + late final Subscription> subscription3; + late final local1 = 'string'; + late final a = Allowed(); +} +$_typesDefinitions + ''', + ); + } + + Future test_reports_non_ignored_types_fields() async { + await assertDiagnostics( + ''' +class Test { + late final String field2; + late final field3; + late final NotAllowed na1; + late final Subscription subscription1; + late final Subscription> subscription7; +} +$_typesDefinitions + ''', + [ + lint(33, 6), + lint(54, 6), + lint(86, 3), + lint(125, 13), + lint(188, 13), + ], + ); + } + + Future test_reports_non_ignored_types_local_variables() async { + await assertDiagnostics( + ''' +void test() { + late final String local2; + late final local3; + late final NotAllowed na1; + late final Subscription subscription1; +} +$_typesDefinitions + ''', + [ + lint(34, 6), + lint(55, 6), + lint(87, 3), + lint(126, 13), + ], + ); + } +} diff --git a/test/src/common/parameter_parser/analysis_options_loader_test.dart b/test/src/common/parameter_parser/analysis_options_loader_test.dart new file mode 100644 index 00000000..a727136f --- /dev/null +++ b/test/src/common/parameter_parser/analysis_options_loader_test.dart @@ -0,0 +1,225 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/workspace/workspace.dart'; +import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:test/test.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AnalysisOptionsLoaderTest); + }); +} + +@reflectiveTest +class AnalysisOptionsLoaderTest extends PubPackageResolutionTest { + // TODO: use actual [Rule.lintName] after migrating to analyzer_server_plugin + // Can't be used right now because they have compile errors + static const _mockRuleThatNeedsConfigName = 'mock_rule_that_needs_config'; + static const _mockRule2Name = 'mock_rule_2'; + static const _cyclomaticComplexityName = 'cyclomatic_complexity'; + + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + abc: def + $_mockRule2Name: + foo: bar + exclude: + - class_name: MockClass + method_name: mockMethod + $_cyclomaticComplexityName: + max_complexity: 10 + exclude: + - class_name: MockClass + method_name: mockMethod + - method_name: mockMethod2 + '''; + static const _mockDifferentAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + abc: ghi + $_mockRule2Name: + foo: baz + exclude: + - class_name: MockOtherClass + method_name: mockOtherMethod + $_cyclomaticComplexityName: + max_complexity: 20 + exclude: + - class_name: MockOtherClass + method_name: mockOtherMethod + - method_name: mockOtherMethod2 + '''; + + late AnalysisOptionsLoader analysisOptionsLoader; + late RuleContext mockRuleContext; + + @override + void setUp() { + super.setUp(); + + analysisOptionsLoader = + AnalysisOptionsLoader(resourceProvider: resourceProvider); + mockRuleContext = _createMockContextForPackage(testPackageRootPath); + + _writeMockAnalysisOptionsYamlFile(); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + } + + void _writeMockAnalysisOptionsYamlFile() { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _mockAnalysisOptionsContent, + ); + } + + void test_cached_response_is_scoped_to_package_and_rule() { + const otherPackageRootPath = '/home/other'; + + newFolder(otherPackageRootPath); + newPubspecYamlFile(otherPackageRootPath, 'name: other'); + newAnalysisOptionsYamlFile( + otherPackageRootPath, + _mockDifferentAnalysisOptionsContent, + ); + + for (final ruleName in [ + _mockRuleThatNeedsConfigName, + _mockRule2Name, + _cyclomaticComplexityName + ]) { + final currentPackageOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + ruleName, + ); + final otherPackageOptions = analysisOptionsLoader.getRuleOptions( + _createMockContextForPackage(otherPackageRootPath), + ruleName, + ); + + expect( + currentPackageOptions, + isNot(equals(otherPackageOptions)), + ); + } + } + + void test_each_rule_gets_its_options() { + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final mockRuleThatNeedsConfigOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + final mockRule2Options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRule2Name, + ); + final cyclomaticComplexityOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _cyclomaticComplexityName, + ); + + expect(mockRuleThatNeedsConfigOptions, isNotNull); + expect(mockRuleThatNeedsConfigOptions, {'abc': 'def'}); + + expect(mockRule2Options, isNotNull); + expect(mockRule2Options, { + 'foo': 'bar', + 'exclude': [ + {'class_name': 'MockClass', 'method_name': 'mockMethod'}, + ] + }); + + expect(cyclomaticComplexityOptions, isNotNull); + expect(cyclomaticComplexityOptions, { + 'max_complexity': 10, + 'exclude': [ + {'class_name': 'MockClass', 'method_name': 'mockMethod'}, + {'method_name': 'mockMethod2'}, + ] + }); + } + + void test_invalidates_cache_when_analysis_options_changed() { + final initialOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + _mockDifferentAnalysisOptionsContent, + ); + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final updatedOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + expect(initialOptions, {'abc': 'def'}); + expect(updatedOptions, {'abc': 'ghi'}); + expect(updatedOptions, isNot(same(initialOptions))); + } + + void test_loads_and_parses_rule_options_from_yaml_file() { + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + expect(options, isNotNull); + expect(options, {'abc': 'def'}); + } + + void test_returns_cached_response_for_same_rule_name() { + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final firstOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + final secondOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + expect(secondOptions, same(firstOptions)); + } + + RuleContext _createMockContextForPackage(String packageRootPath) { + return _TestRuleContext( + _TestWorkspacePackage(getFolder(packageRootPath)), + ); + } +} + +class _TestRuleContext implements RuleContext { + @override + final WorkspacePackage? package; + + _TestRuleContext(this.package); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _TestWorkspacePackage implements WorkspacePackage { + @override + final Folder root; + + _TestWorkspacePackage(this.root); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +}