Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lib/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class SolidLintsPlugin extends Plugin {
final lintRules = [
AvoidFinalWithGetterRule(),
AvoidGlobalStateRule(),
AvoidNonNullAssertionRule(),
AvoidNonNullAssertionRule(analysisOptionsLoader: analysisLoader),
avoidUnnecessaryTypeAssertionsRule,
AvoidDebugPrintInReleaseRule(),
doubleLiteralFormatRule,
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<AvoidNonNullAssertionParameters> {
/// Name of the lint
static const String lintName = 'avoid_non_null_assertion';

Expand All @@ -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
Expand All @@ -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),
);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> map;
/// map['key']!; // OK
/// ```
final Set<String> 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<String, Object?> json) {
final raw = json['ignored_types'];
final excludeList = switch (raw) {
final Iterable<Object?> rawList => rawList.whereType<String>().toSet(),
final Map<Object?, Object?> rawMap =>
rawMap.keys.whereType<String>().toSet(),
final String rawString => {rawString},
_ => const <String>{},
};
Comment thread
solid-illiaaihistov marked this conversation as resolved.

return AvoidNonNullAssertionParameters(
ignoredTypes: excludeList,
);
}
Comment thread
solid-illiaaihistov marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,31 @@ 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<void> {
/// 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) {
if (node.operator.type != TokenType.BANG) {
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;
}
}
Expand All @@ -38,4 +42,14 @@ class AvoidNonNullAssertionVisitor extends SimpleAstVisitor<void> {

return type.isDartCoreMap || type.allSupertypes.any((v) => v.isDartCoreMap);
}

bool _hasIgnoredType(DartType? type) {
if (type == null) {
return false;
}

return type.hasIgnoredType(
ignoredTypes: _parameters.ignoredTypes,
);
Comment thread
solid-illiaaihistov marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
@@ -1,44 +1,63 @@
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);
});
}

@reflectiveTest
class AvoidNonNullAssertionRuleTest extends AnalysisRuleTest {
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''',
);
}

void test_reports_non_null_assertion_on_nullable_value() async {
await assertDiagnostics(
r'''
Future<void> 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<void> 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<void> test_does_not_report_map_access() async {
await assertNoDiagnostics(r'''
void m() {
final map = {'key': 'value'};
Expand All @@ -47,13 +66,55 @@ void m() {
''');
}

void test_does_not_report_safe_null_check() async {
Future<void> test_does_not_report_safe_null_check() async {
await assertNoDiagnostics(r'''
void m(int? number) {
if (number != null) {
final value = number;
}
}
''');
}

Future<void> test_does_not_report_imap_access() async {
await assertNoDiagnostics(r'''
class IMap<K, V> {
V? operator [](K key) => null;
}

void m(IMap<String, String> map) {
map['key']!;
}
''');
}

Future<void> test_does_not_report_parenthesized_map_access() async {
await assertNoDiagnostics(r'''
void m() {
final map = {'key': 'value'};
(map['key'])!;
}
''');
}

Future<void> 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<K, V> {
V? operator [](K key) => null;
}

void m(IMap<String, String> map) {
map['key']!;
}
''');
}
}
Loading