diff --git a/lib/analysis_options.yaml b/lib/analysis_options.yaml index 269a6441..dd2a9c95 100644 --- a/lib/analysis_options.yaml +++ b/lib/analysis_options.yaml @@ -43,7 +43,10 @@ solid_lints: allow_initialized: true ignored_types: - AnimationController - avoid_non_null_assertion: true + avoid_non_null_assertion: + ignored_types: + - IMap + - BuiltMap avoid_returning_widgets: true avoid_unnecessary_return_variable: true avoid_unnecessary_setstate: true diff --git a/lib/main.dart b/lib/main.dart index 4ab02252..43c08108 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -66,7 +66,7 @@ class SolidLintsPlugin extends Plugin { final lintRules = [ AvoidFinalWithGetterRule(), AvoidGlobalStateRule(), - AvoidNonNullAssertionRule(), + AvoidNonNullAssertionRule(analysisOptionsLoader: analysisLoader), avoidUnnecessaryTypeAssertionsRule, AvoidDebugPrintInReleaseRule(), doubleLiteralFormatRule, diff --git a/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart b/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart index 349297cd..171cbc29 100644 --- a/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart +++ b/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart @@ -1,8 +1,9 @@ -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/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart'; +import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Rule which warns about usages of bang operator ("!") /// as it may result in unexpected runtime exceptions. @@ -35,7 +36,8 @@ import 'package:solid_lints/src/lints/avoid_non_null_assertion/visitors/avoid_no /// final map = {'key': 'value'}; /// map['key']!; /// ``` -class AvoidNonNullAssertionRule extends AnalysisRule { +class AvoidNonNullAssertionRule + extends SolidLintRule { /// Name of the lint static const String lintName = 'avoid_non_null_assertion'; @@ -46,11 +48,12 @@ class AvoidNonNullAssertionRule extends AnalysisRule { ); /// creates an instance of [AvoidNonNullAssertionRule] - AvoidNonNullAssertionRule() - : super( + AvoidNonNullAssertionRule({required super.analysisOptionsLoader}) + : super.withParameters( name: lintName, description: 'Warns about usages of bang operator (!) except valid Map access.', + parametersParser: AvoidNonNullAssertionParameters.fromJson, ); @override @@ -61,6 +64,13 @@ class AvoidNonNullAssertionRule extends AnalysisRule { RuleVisitorRegistry registry, RuleContext context, ) { - registry.addPostfixExpression(this, AvoidNonNullAssertionVisitor(this)); + final parameters = + getParametersForContext(context) ?? + AvoidNonNullAssertionParameters.empty(); + + registry.addPostfixExpression( + this, + AvoidNonNullAssertionVisitor(this, parameters), + ); } } diff --git a/lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart b/lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart new file mode 100644 index 00000000..e10755e4 --- /dev/null +++ b/lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart @@ -0,0 +1,49 @@ +/// A data model class that represents the "avoid non null assertion" input +/// parameters. +class AvoidNonNullAssertionParameters { + /// Types that would be ignored by avoid-non-null-assertion rule + /// + /// Example: + /// + /// ```yaml + /// plugins: + /// solid_lints: + /// diagnostics: + /// avoid_non_null_assertion: + /// ignored_types: + /// - IMap + /// ``` + /// + /// ```dart + /// IMap map; + /// map['key']!; // OK + /// ``` + final Set ignoredTypes; + + /// Constructor for [AvoidNonNullAssertionParameters] model + const AvoidNonNullAssertionParameters({ + required this.ignoredTypes, + }); + + /// Empty [AvoidNonNullAssertionParameters] model, ignores nothing. + factory AvoidNonNullAssertionParameters.empty() => + const AvoidNonNullAssertionParameters( + ignoredTypes: {}, + ); + + /// Method for creating from json data + factory AvoidNonNullAssertionParameters.fromJson(Map json) { + final raw = json['ignored_types']; + final excludeList = switch (raw) { + final Iterable rawList => rawList.whereType().toSet(), + final Map rawMap => + rawMap.keys.whereType().toSet(), + final String rawString => {rawString}, + _ => const {}, + }; + + return AvoidNonNullAssertionParameters( + ignoredTypes: excludeList, + ); + } +} diff --git a/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart b/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart index 1dd32ae2..5e27f29c 100644 --- a/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart +++ b/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart @@ -3,14 +3,18 @@ import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; +import 'package:solid_lints/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; /// visitor for [AvoidNonNullAssertionRule] class AvoidNonNullAssertionVisitor extends SimpleAstVisitor { /// Rule associated with this visitor final AvoidNonNullAssertionRule rule; + final AvoidNonNullAssertionParameters _parameters; + /// Creates an instance of [AvoidNonNullAssertionVisitor] - AvoidNonNullAssertionVisitor(this.rule); + AvoidNonNullAssertionVisitor(this.rule, this._parameters); @override void visitPostfixExpression(PostfixExpression node) { @@ -18,12 +22,12 @@ class AvoidNonNullAssertionVisitor extends SimpleAstVisitor { return; } - final operand = node.operand; + final operand = node.operand.unParenthesized; if (operand is IndexExpression) { final type = operand.target?.staticType; - if (_isMap(type)) { + if (_isMap(type) || _hasIgnoredType(type)) { return; } } @@ -38,4 +42,14 @@ class AvoidNonNullAssertionVisitor extends SimpleAstVisitor { return type.isDartCoreMap || type.allSupertypes.any((v) => v.isDartCoreMap); } + + bool _hasIgnoredType(DartType? type) { + if (type == null) { + return false; + } + + return type.hasIgnoredType( + ignoredTypes: _parameters.ignoredTypes, + ); + } } diff --git a/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart b/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart index 10cf6ced..001cf982 100644 --- a/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart +++ b/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart @@ -1,4 +1,6 @@ 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_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; @@ -13,10 +15,30 @@ void main() { @reflectiveTest class AvoidNonNullAssertionRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + avoid_non_null_assertion: + ignored_types: + - IMap + - BuiltMap + '''; + @override void setUp() { - rule = AvoidNonNullAssertionRule(); + rule = AvoidNonNullAssertionRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); } Future test_reports_non_null_assertion_on_nullable_value() async { @@ -51,6 +73,48 @@ void m(int? number) { final value = number; } } +'''); + } + + Future test_does_not_report_imap_access() async { + await assertNoDiagnostics(r''' +class IMap { + V? operator [](K key) => null; +} + +void m(IMap map) { + map['key']!; +} +'''); + } + + Future test_does_not_report_parenthesized_map_access() async { + await assertNoDiagnostics(r''' +void m() { + final map = {'key': 'value'}; + (map['key'])!; +} +'''); + } + + Future test_does_not_report_imap_access_with_single_string() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + avoid_non_null_assertion: + ignored_types: IMap''', + ); + await assertNoDiagnostics(r''' +class IMap { + V? operator [](K key) => null; +} + +void m(IMap map) { + map['key']!; +} '''); } }