From 9789fa47f044f8c9f33c411e09a933d6f98e90dd Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 3 Jul 2026 10:53:23 +0300 Subject: [PATCH 1/5] feat: add ignored_types configuration to avoid_non_null_assertion lint rule --- lib/analysis_options.yaml | 5 +- lib/main.dart | 2 +- .../avoid_non_null_assertion_rule.dart | 20 ++++-- .../avoid_non_null_assertion_parameters.dart | 41 +++++++++++ .../avoid_non_null_assertion_visitor.dart | 20 +++++- .../avoid_non_null_assertion_rule_test.dart | 71 ++++++++++++++----- 6 files changed, 131 insertions(+), 28 deletions(-) create mode 100644 lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart diff --git a/lib/analysis_options.yaml b/lib/analysis_options.yaml index 21857abc..4059d05e 100644 --- a/lib/analysis_options.yaml +++ b/lib/analysis_options.yaml @@ -44,7 +44,10 @@ custom_lint: ignored_types: - AnimationController - - avoid_non_null_assertion + - avoid_non_null_assertion: + ignored_types: + - IMap + - BuiltMap - avoid_returning_widgets - avoid_unnecessary_return_variable - avoid_unnecessary_setstate diff --git a/lib/main.dart b/lib/main.dart index cecaeb70..ee8f6217 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -60,7 +60,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..4ce4adc3 --- /dev/null +++ b/lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart @@ -0,0 +1,41 @@ +/// 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 Iterable 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) => + AvoidNonNullAssertionParameters( + ignoredTypes: List.from( + json['ignored_types'] as Iterable? ?? [], + ), + ); +} 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..30f7fb62 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.toSet(), + ); + } } 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 8d054cda..d7e17e45 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,7 +1,11 @@ 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'; +import '../auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(AvoidNonNullAssertionRuleTest); @@ -9,36 +13,46 @@ void main() { } @reflectiveTest -class AvoidNonNullAssertionRuleTest extends AnalysisRuleTest { +class AvoidNonNullAssertionRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { - rule = AvoidNonNullAssertionRule(); + rule = AvoidNonNullAssertionRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); super.setUp(); + + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + ${rule.name}: + ignored_types: + - IMap + - BuiltMap +'''); } - void test_reports_non_null_assertion_on_nullable_value() async { - await assertDiagnostics( - r''' + Future test_reports_non_null_assertion_on_nullable_value() async { + await assertAutoDiagnostics(''' void m(int? number) { - final value = number!; + final value = ${expectLint('number!')}; } -''', - [lint(38, 7)], - ); +'''); } - void test_reports_non_null_assertion_on_method_call() async { - await assertDiagnostics( - r''' + Future test_reports_non_null_assertion_on_method_call() async { + await assertAutoDiagnostics(''' void m(Object? object) { - object!.toString(); + ${expectLint('object!')}.toString(); } -''', - [lint(27, 7)], - ); +'''); } - void test_does_not_report_map_access() async { + Future test_does_not_report_map_access() async { await assertNoDiagnostics(r''' void m() { final map = {'key': 'value'}; @@ -47,13 +61,34 @@ void m() { '''); } - void test_does_not_report_safe_null_check() async { + Future test_does_not_report_safe_null_check() async { await assertNoDiagnostics(r''' void m(int? number) { if (number != null) { 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'])!; +} '''); } } From f36dbe793b59a886732610423dcbe80f72d803ad Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 3 Jul 2026 11:45:02 +0300 Subject: [PATCH 2/5] refactor: extract analysis options configuration to a constant in avoid_non_null_assertion test --- .../avoid_non_null_assertion_rule_test.dart | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) 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 d7e17e45..fbff950c 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 @@ -15,6 +15,16 @@ 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( @@ -24,16 +34,11 @@ class AvoidNonNullAssertionRuleTest extends AnalysisRuleTest ); super.setUp(); - newAnalysisOptionsYamlFile(testPackageRootPath, ''' -${analysisOptionsContent(rules: [rule.name])} -plugins: - solid_lints: - diagnostics: - ${rule.name}: - ignored_types: - - IMap - - BuiltMap -'''); + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); } Future test_reports_non_null_assertion_on_nullable_value() async { From ec6a1e5971b0bc2ef8c964db4047819a26056cb3 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 3 Jul 2026 18:45:29 +0300 Subject: [PATCH 3/5] refactor: update ignoredTypes to a Set and improve JSON parsing to support string or map inputs --- .../avoid_non_null_assertion_parameters.dart | 22 ++++++++++++------- .../avoid_non_null_assertion_visitor.dart | 2 +- .../avoid_non_null_assertion_rule_test.dart | 21 ++++++++++++++++++ 3 files changed, 36 insertions(+), 9 deletions(-) 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 index 4ce4adc3..2092df40 100644 --- 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 @@ -18,7 +18,7 @@ class AvoidNonNullAssertionParameters { /// IMap map; /// map['key']!; // OK /// ``` - final Iterable ignoredTypes; + final Set ignoredTypes; /// Constructor for [AvoidNonNullAssertionParameters] model const AvoidNonNullAssertionParameters({ @@ -28,14 +28,20 @@ class AvoidNonNullAssertionParameters { /// Empty [AvoidNonNullAssertionParameters] model, ignores nothing. factory AvoidNonNullAssertionParameters.empty() => const AvoidNonNullAssertionParameters( - ignoredTypes: [], + ignoredTypes: {}, ); /// Method for creating from json data - factory AvoidNonNullAssertionParameters.fromJson(Map json) => - AvoidNonNullAssertionParameters( - ignoredTypes: List.from( - json['ignored_types'] as Iterable? ?? [], - ), - ); + factory AvoidNonNullAssertionParameters.fromJson(Map json) { + final raw = json['ignored_types']; + final excludeList = switch (raw) { + Iterable() => raw.whereType().toSet(), + Map() || String() => {raw.toString()}, + _ => 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 30f7fb62..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 @@ -49,7 +49,7 @@ class AvoidNonNullAssertionVisitor extends SimpleAstVisitor { } return type.hasIgnoredType( - ignoredTypes: _parameters.ignoredTypes.toSet(), + 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 fbff950c..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 @@ -94,6 +94,27 @@ 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']!; +} '''); } } From 17afd43364f63e3d6b26a4100dddfea0acede5ee Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 3 Jul 2026 18:54:52 +0300 Subject: [PATCH 4/5] refactor: update avoid_non_null_assertion parameter parsing to correctly extract keys from maps --- .../models/avoid_non_null_assertion_parameters.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 2092df40..b4d734aa 100644 --- 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 @@ -36,7 +36,8 @@ class AvoidNonNullAssertionParameters { final raw = json['ignored_types']; final excludeList = switch (raw) { Iterable() => raw.whereType().toSet(), - Map() || String() => {raw.toString()}, + Map() => raw.keys.whereType().toSet(), + String() => {raw}, _ => const {}, }; From 29c884e589b582c9573c05167a16ac110bfe42df Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 3 Jul 2026 19:01:49 +0300 Subject: [PATCH 5/5] refactor: improve type safety and readability of AvoidNonNullAssertionParameters JSON decoding using typed patterns --- .../models/avoid_non_null_assertion_parameters.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 index b4d734aa..e10755e4 100644 --- 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 @@ -35,9 +35,10 @@ class AvoidNonNullAssertionParameters { factory AvoidNonNullAssertionParameters.fromJson(Map json) { final raw = json['ignored_types']; final excludeList = switch (raw) { - Iterable() => raw.whereType().toSet(), - Map() => raw.keys.whereType().toSet(), - String() => {raw}, + final Iterable rawList => rawList.whereType().toSet(), + final Map rawMap => + rawMap.keys.whereType().toSet(), + final String rawString => {rawString}, _ => const {}, };