Skip to content

Commit 562cddd

Browse files
solid-illiaaihistovIllia Aihistov
andauthored
feat: avoid non null assertion ignored types (#313)
* feat: add ignored_types configuration to avoid_non_null_assertion lint rule * refactor: extract analysis options configuration to a constant in avoid_non_null_assertion test * refactor: update ignoredTypes to a Set and improve JSON parsing to support string or map inputs * refactor: update avoid_non_null_assertion parameter parsing to correctly extract keys from maps * refactor: improve type safety and readability of AvoidNonNullAssertionParameters JSON decoding using typed patterns --------- Co-authored-by: Illia Aihistov <illia.aihistov-us@solid.software>
1 parent 0376fd6 commit 562cddd

6 files changed

Lines changed: 151 additions & 11 deletions

File tree

lib/analysis_options.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ solid_lints:
4343
allow_initialized: true
4444
ignored_types:
4545
- AnimationController
46-
avoid_non_null_assertion: true
46+
avoid_non_null_assertion:
47+
ignored_types:
48+
- IMap
49+
- BuiltMap
4750
avoid_returning_widgets: true
4851
avoid_unnecessary_return_variable: true
4952
avoid_unnecessary_setstate: true

lib/main.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class SolidLintsPlugin extends Plugin {
6666
final lintRules = [
6767
AvoidFinalWithGetterRule(),
6868
AvoidGlobalStateRule(),
69-
AvoidNonNullAssertionRule(),
69+
AvoidNonNullAssertionRule(analysisOptionsLoader: analysisLoader),
7070
avoidUnnecessaryTypeAssertionsRule,
7171
AvoidDebugPrintInReleaseRule(),
7272
doubleLiteralFormatRule,

lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import 'package:analyzer/analysis_rule/analysis_rule.dart';
21
import 'package:analyzer/analysis_rule/rule_context.dart';
32
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
43
import 'package:analyzer/error/error.dart';
4+
import 'package:solid_lints/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart';
55
import 'package:solid_lints/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart';
6+
import 'package:solid_lints/src/models/solid_lint_rule.dart';
67

78
/// Rule which warns about usages of bang operator ("!")
89
/// 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
3536
/// final map = {'key': 'value'};
3637
/// map['key']!;
3738
/// ```
38-
class AvoidNonNullAssertionRule extends AnalysisRule {
39+
class AvoidNonNullAssertionRule
40+
extends SolidLintRule<AvoidNonNullAssertionParameters> {
3941
/// Name of the lint
4042
static const String lintName = 'avoid_non_null_assertion';
4143

@@ -46,11 +48,12 @@ class AvoidNonNullAssertionRule extends AnalysisRule {
4648
);
4749

4850
/// creates an instance of [AvoidNonNullAssertionRule]
49-
AvoidNonNullAssertionRule()
50-
: super(
51+
AvoidNonNullAssertionRule({required super.analysisOptionsLoader})
52+
: super.withParameters(
5153
name: lintName,
5254
description:
5355
'Warns about usages of bang operator (!) except valid Map access.',
56+
parametersParser: AvoidNonNullAssertionParameters.fromJson,
5457
);
5558

5659
@override
@@ -61,6 +64,13 @@ class AvoidNonNullAssertionRule extends AnalysisRule {
6164
RuleVisitorRegistry registry,
6265
RuleContext context,
6366
) {
64-
registry.addPostfixExpression(this, AvoidNonNullAssertionVisitor(this));
67+
final parameters =
68+
getParametersForContext(context) ??
69+
AvoidNonNullAssertionParameters.empty();
70+
71+
registry.addPostfixExpression(
72+
this,
73+
AvoidNonNullAssertionVisitor(this, parameters),
74+
);
6575
}
6676
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/// A data model class that represents the "avoid non null assertion" input
2+
/// parameters.
3+
class AvoidNonNullAssertionParameters {
4+
/// Types that would be ignored by avoid-non-null-assertion rule
5+
///
6+
/// Example:
7+
///
8+
/// ```yaml
9+
/// plugins:
10+
/// solid_lints:
11+
/// diagnostics:
12+
/// avoid_non_null_assertion:
13+
/// ignored_types:
14+
/// - IMap
15+
/// ```
16+
///
17+
/// ```dart
18+
/// IMap<String, String> map;
19+
/// map['key']!; // OK
20+
/// ```
21+
final Set<String> ignoredTypes;
22+
23+
/// Constructor for [AvoidNonNullAssertionParameters] model
24+
const AvoidNonNullAssertionParameters({
25+
required this.ignoredTypes,
26+
});
27+
28+
/// Empty [AvoidNonNullAssertionParameters] model, ignores nothing.
29+
factory AvoidNonNullAssertionParameters.empty() =>
30+
const AvoidNonNullAssertionParameters(
31+
ignoredTypes: {},
32+
);
33+
34+
/// Method for creating from json data
35+
factory AvoidNonNullAssertionParameters.fromJson(Map<String, Object?> json) {
36+
final raw = json['ignored_types'];
37+
final excludeList = switch (raw) {
38+
final Iterable<Object?> rawList => rawList.whereType<String>().toSet(),
39+
final Map<Object?, Object?> rawMap =>
40+
rawMap.keys.whereType<String>().toSet(),
41+
final String rawString => {rawString},
42+
_ => const <String>{},
43+
};
44+
45+
return AvoidNonNullAssertionParameters(
46+
ignoredTypes: excludeList,
47+
);
48+
}
49+
}

lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,31 @@ import 'package:analyzer/dart/ast/token.dart';
33
import 'package:analyzer/dart/ast/visitor.dart';
44
import 'package:analyzer/dart/element/type.dart';
55
import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart';
6+
import 'package:solid_lints/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart';
7+
import 'package:solid_lints/src/utils/types_utils.dart';
68

79
/// visitor for [AvoidNonNullAssertionRule]
810
class AvoidNonNullAssertionVisitor extends SimpleAstVisitor<void> {
911
/// Rule associated with this visitor
1012
final AvoidNonNullAssertionRule rule;
1113

14+
final AvoidNonNullAssertionParameters _parameters;
15+
1216
/// Creates an instance of [AvoidNonNullAssertionVisitor]
13-
AvoidNonNullAssertionVisitor(this.rule);
17+
AvoidNonNullAssertionVisitor(this.rule, this._parameters);
1418

1519
@override
1620
void visitPostfixExpression(PostfixExpression node) {
1721
if (node.operator.type != TokenType.BANG) {
1822
return;
1923
}
2024

21-
final operand = node.operand;
25+
final operand = node.operand.unParenthesized;
2226

2327
if (operand is IndexExpression) {
2428
final type = operand.target?.staticType;
2529

26-
if (_isMap(type)) {
30+
if (_isMap(type) || _hasIgnoredType(type)) {
2731
return;
2832
}
2933
}
@@ -38,4 +42,14 @@ class AvoidNonNullAssertionVisitor extends SimpleAstVisitor<void> {
3842

3943
return type.isDartCoreMap || type.allSupertypes.any((v) => v.isDartCoreMap);
4044
}
45+
46+
bool _hasIgnoredType(DartType? type) {
47+
if (type == null) {
48+
return false;
49+
}
50+
51+
return type.hasIgnoredType(
52+
ignoredTypes: _parameters.ignoredTypes,
53+
);
54+
}
4155
}

test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
2+
import 'package:analyzer_testing/utilities/utilities.dart';
3+
import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart';
24
import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart';
35
import 'package:test_reflective_loader/test_reflective_loader.dart';
46

@@ -13,10 +15,30 @@ void main() {
1315
@reflectiveTest
1416
class AvoidNonNullAssertionRuleTest extends AnalysisRuleTest
1517
with AutoTestLintOffsets {
18+
static const _mockAnalysisOptionsContent = '''
19+
plugins:
20+
solid_lints:
21+
diagnostics:
22+
avoid_non_null_assertion:
23+
ignored_types:
24+
- IMap
25+
- BuiltMap
26+
''';
27+
1628
@override
1729
void setUp() {
18-
rule = AvoidNonNullAssertionRule();
30+
rule = AvoidNonNullAssertionRule(
31+
analysisOptionsLoader: AnalysisOptionsLoader(
32+
resourceProvider: resourceProvider,
33+
),
34+
);
1935
super.setUp();
36+
37+
newAnalysisOptionsYamlFile(
38+
testPackageRootPath,
39+
'''${analysisOptionsContent(rules: [rule.name])}
40+
$_mockAnalysisOptionsContent''',
41+
);
2042
}
2143

2244
Future<void> test_reports_non_null_assertion_on_nullable_value() async {
@@ -51,6 +73,48 @@ void m(int? number) {
5173
final value = number;
5274
}
5375
}
76+
''');
77+
}
78+
79+
Future<void> test_does_not_report_imap_access() async {
80+
await assertNoDiagnostics(r'''
81+
class IMap<K, V> {
82+
V? operator [](K key) => null;
83+
}
84+
85+
void m(IMap<String, String> map) {
86+
map['key']!;
87+
}
88+
''');
89+
}
90+
91+
Future<void> test_does_not_report_parenthesized_map_access() async {
92+
await assertNoDiagnostics(r'''
93+
void m() {
94+
final map = {'key': 'value'};
95+
(map['key'])!;
96+
}
97+
''');
98+
}
99+
100+
Future<void> test_does_not_report_imap_access_with_single_string() async {
101+
newAnalysisOptionsYamlFile(
102+
testPackageRootPath,
103+
'''${analysisOptionsContent(rules: [rule.name])}
104+
plugins:
105+
solid_lints:
106+
diagnostics:
107+
avoid_non_null_assertion:
108+
ignored_types: IMap''',
109+
);
110+
await assertNoDiagnostics(r'''
111+
class IMap<K, V> {
112+
V? operator [](K key) => null;
113+
}
114+
115+
void m(IMap<String, String> map) {
116+
map['key']!;
117+
}
54118
''');
55119
}
56120
}

0 commit comments

Comments
 (0)