Skip to content
This repository has been archived by the owner on Jul 16, 2023. It is now read-only.

Add avoid non null assertion rule #285

Merged
merged 5 commits into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

* Add static code diagnostic `avoid-non-null-assertion`.
* Migrate all rule tests to `resolveFile`.
* Improve static code diagnostics `no-equal-arguments`, `no-magic-number`.

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ Rules configuration is [described here](#configuring-a-rules-entry).

### Common

- [avoid-non-null-assertion](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/avoid_non_null_assertion.md)
- [avoid-unused-parameters](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/avoid_unused_parameters.md)
- [binary-expression-operand-order](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/binary_expression_operand_order.md)   ![Has auto-fix](https://img.shields.io/badge/-has%20auto--fix-success)
- [double-literal-format](https://github.com/dart-code-checker/dart-code-metrics/blob/master/doc/rules/double_literal_format.md)   ![Has auto-fix](https://img.shields.io/badge/-has%20auto--fix-success)
Expand Down
57 changes: 57 additions & 0 deletions doc/rules/avoid_non_null_assertion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Avoid non null assertion

## Rule id

avoid-non-null-assertion

## Description

Warns when non null assertion operator (**!** or “bang” operator) is used for a property access or method invocation. The operator check works at runtime and it may fail and throw a runtime exception.

The rule ignores the index `[]` operator on the Map class because it's considered the idiomatic way to access a known-present element in a map with `[]!` according to [the docs](https://dart.dev/null-safety/understanding-null-safety#the-map-index-operator-is-nullable).

Use this rule if you want to avoid possible unexpected runtime exceptions.

### Example

Bad:

```dart
class Test {
String? field;

Test? object;

void method() {
field!.contains('other'); // LINT

object!.field!.contains('other'); // LINT

final map = {'key': 'value'};
map['key']!.contains('other');

object!.method(); // LINT
}
}
```

Good:

```dart
class Test {
String? field;

Test? object;

void method() {
field?.contains('other');

object?.field?.contains('other');

final map = {'key': 'value'};
map['key']!.contains('other');

object?.method();
}
}
```
70 changes: 70 additions & 0 deletions lib/src/obsoleted/rules/avoid_non_null_assertion_rule.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';

import '../../models/issue.dart';
import '../../models/severity.dart';
import '../../utils/node_utils.dart';
import '../../utils/rule_utils.dart';
import 'obsolete_rule.dart';

class AvoidNonNullAssertionRule extends ObsoleteRule {
static const String ruleId = 'avoid-non-null-assertion';
static const _documentationUrl = 'https://git.io/JO5Ju';

static const _failure = 'Avoid using non null assertion.';

AvoidNonNullAssertionRule({Map<String, Object> config = const {}})
: super(
id: ruleId,
documentationUrl: Uri.parse(_documentationUrl),
severity: readSeverity(config, Severity.warning),
excludes: readExcludes(config),
);

@override
Iterable<Issue> check(ResolvedUnitResult source) {
final visitor = _Visitor();

source.unit?.visitChildren(visitor);

return visitor.expressions
.map((expression) => createIssue(
rule: this,
location: nodeLocation(
node: expression,
source: source,
withCommentOrMetadata: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@incendial please remove withCommentOrMetadata: true,

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

),
message: _failure,
))
.toList(growable: false);
}
}

class _Visitor extends RecursiveAstVisitor<void> {
final _expressions = <Expression>[];

Iterable<Expression> get expressions => _expressions;

@override
void visitPostfixExpression(PostfixExpression node) {
super.visitPostfixExpression(node);

if (node.operator.type == TokenType.BANG &&
!_isMapIndexOperator(node.operand)) {
_expressions.add(node);
}
}

bool _isMapIndexOperator(Expression operand) {
if (operand is IndexExpression) {
final type = operand.target?.staticType;

return type != null && type.isDartCoreMap;
}

return false;
}
}
3 changes: 3 additions & 0 deletions lib/src/obsoleted/rules_factory.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '../rules/rule.dart';
import 'rules/avoid_non_null_assertion_rule.dart';
import 'rules/avoid_preserve_whitespace_false.dart';
import 'rules/avoid_returning_widgets_rule.dart';
import 'rules/avoid_unused_parameters.dart';
Expand All @@ -21,6 +22,8 @@ import 'rules/prefer_trailing_comma.dart';
import 'rules/provide_correct_intl_args.dart';

final _implementedRules = <String, Rule Function(Map<String, Object>)>{
AvoidNonNullAssertionRule.ruleId: (config) =>
AvoidNonNullAssertionRule(config: config),
AvoidPreserveWhitespaceFalseRule.ruleId: (config) =>
AvoidPreserveWhitespaceFalseRule(config: config),
AvoidReturningWidgets.ruleId: (config) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
@TestOn('vm')
import 'package:dart_code_metrics/src/models/severity.dart';
import 'package:dart_code_metrics/src/obsoleted/rules/avoid_non_null_assertion_rule.dart';
import 'package:test/test.dart';

import '../../../helpers/rule_test_helper.dart';

const _examplePath =
'test/obsoleted/rules/avoid_non_null_assertion/examples/example.dart';

void main() {
group('AvoidNonNullAssertion', () {
test('initialization', () async {
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
final issues = AvoidNonNullAssertionRule().check(unit);

RuleTestHelper.verifyInitialization(
issues: issues,
ruleId: 'avoid-non-null-assertion',
severity: Severity.warning,
);
});

test('reports about found issues', () async {
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
final issues = AvoidNonNullAssertionRule().check(unit);

RuleTestHelper.verifyIssues(
issues: issues,
startOffsets: [70, 208, 208, 460],
startLines: [7, 15, 15, 27],
startColumns: [5, 5, 5, 5],
endOffsets: [76, 215, 222, 467],
locationTexts: [
'field!',
'object!',
'object!.field!',
'object!',
],
messages: [
'Avoid using non null assertion.',
'Avoid using non null assertion.',
'Avoid using non null assertion.',
'Avoid using non null assertion.',
],
);
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class Test {
String? field;

Test? object;

void method() {
field!.contains('other'); // LINT

field?.replaceAll('from', 'replace');

if (filed != null) {
field.split(' ');
}

object!.field!.contains('other'); // LINT

object?.field?.contains('other');

final field = object?.field;
if (field != null) {
field.contains('other');
}

final map = {'key': 'value'};
map['key']!.contains('other');

object!.method(); // LINT

object?.method();
}
}