Skip to content

Commit

Permalink
feat: add rule for no_force_unwrap
Browse files Browse the repository at this point in the history
  • Loading branch information
ostk0069 committed Mar 9, 2025
1 parent 8a41599 commit 61b896d
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 0 deletions.
44 changes: 44 additions & 0 deletions packages/nilts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Some of lint rules support quick fixes on IDE.
| [no_support_web_platform_check](#no_support_web_platform_check) | Checks if `Platform.isXxx` usages. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [shrink_wrapped_scroll_view](#shrink_wrapped_scroll_view) | Checks the content of the scroll view is shrink wrapped. | Any versions nilts supports | Practice | Experimental | ✅️ |
| [unnecessary_rebuilds_from_media_query](#unnecessary_rebuilds_from_media_query) | Checks `MediaQuery.xxxOf(context)` or `MediaQuery.maybeXxxOf(context)` usages. | >= Flutter 3.10.0 (Dart 3.0.0) | Practice | Experimental | ✅️ |
| [no_force_unwrap](#no_force_unwrap) | Checks usage of the `!` operator for forced unwrapping. | Any versions nilts supports | Practice | Experimental | ✅️ |

### Details

Expand Down Expand Up @@ -725,6 +726,49 @@ See also:

</details>

### no_force_unwrap

<details>

<!-- prettier-ignore-start -->
- Target SDK : Any versions nilts supports
- Rule type : Practice
- Maturity level : Experimental
- Quick fix : ✅ (pattern matching is available on Dart 3.0.0 and above)
<!-- prettier-ignore-end -->

**Prefer** using null coalescing operator or pattern matching instead of force unwrapping with `!` operator.

**BAD:**
<!-- prettier-ignore-start -->
```dart
final value = someValue!;
```
<!-- prettier-ignore-end -->

**GOOD:**

<!-- prettier-ignore-start -->
```dart
final value = someValue ?? /* Replace with a suitable default value */;
```
<!-- prettier-ignore-end -->

**GOOD(using Pattern Matching on Dart 3.0.0 and above):**

<!-- prettier-ignore-start -->
```dart
if (someValue case final actualValue) {
print('取得した値: $actualValue');
}
```
<!-- prettier-ignore-end -->

See also:

- [Null coalescing operator - Dart language specification](https://dart.dev/language/operators#null-coalescing-operator)
- [Pattern matching - Dart language specification](https://dart.dev/language/patterns)

## Assists

Upcoming... 🚀
Expand Down
2 changes: 2 additions & 0 deletions packages/nilts/lib/nilts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:nilts/src/lints/defined_void_callback_type.dart';
import 'package:nilts/src/lints/fixed_text_scale_rich_text.dart';
import 'package:nilts/src/lints/flaky_tests_with_set_up_all.dart';
import 'package:nilts/src/lints/low_readability_numeric_literals.dart';
import 'package:nilts/src/lints/no_force_unwrap.dart';
import 'package:nilts/src/lints/no_support_multi_text_direction.dart';
import 'package:nilts/src/lints/no_support_web_platform_check.dart';
import 'package:nilts/src/lints/shrink_wrapped_scroll_view.dart';
Expand Down Expand Up @@ -43,5 +44,6 @@ class _NiltsLint extends PluginBase {
const ShrinkWrappedScrollView(),
if (_dartVersion >= const DartVersion(major: 3, minor: 0, patch: 0))
UnnecessaryRebuildsFromMediaQuery(_dartVersion),
NoForceUnwrap(_dartVersion),
];
}
6 changes: 6 additions & 0 deletions packages/nilts/lib/src/change_priority.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,10 @@ class ChangePriority {

/// The priority for [_UnwrapSetUpAll].
static const int unwrapSetUpAll = 90;

/// The priority for [_AddNullCoalescingOperator].
static const int addNullCoalescingOperator = 90;

/// The priority for [_AddPatternMatching].
static const int addPatternMatching = 100;
}
126 changes: 126 additions & 0 deletions packages/nilts/lib/src/lints/no_force_unwrap.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/error/error.dart' as analyzer;
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:nilts/src/change_priority.dart';
import 'package:nilts_core/nilts_core.dart';

/// A class for `no_force_unwrap` rule.
///
/// This rule checks if `!` operator is used to force unwrap a value.
///
/// - Target SDK : Any versions nilts supports
/// - Rule type : Practice
/// - Maturity level : Experimental
/// - Quick fix : ✅ (pattern matching is available on Dart 3.0.0 and above)
///
/// **Consider** using null coalescing operator or pattern matching instead of
/// force unwrapping.
///
/// **BAD:**
/// ```dart
/// final value = someValue!;
/// ```
///
/// **GOOD:**
/// ```dart
/// final value = someValue ?? /* Replace with a suitable default value */;
/// ```
///
/// **GOOD:**
/// ```dart
/// if (someValue case final value) return value;
/// ```
///
/// See also:
///
/// - [Null coalescing operator - Dart language specification](https://dart.dev/language/operators#null-coalescing-operator)
/// - [Pattern matching - Dart language specification](https://dart.dev/language/patterns)
class NoForceUnwrap extends DartLintRule {
/// Create a new instance of [NoForceUnwrap].
const NoForceUnwrap(this._dartVersion) : super(code: _code);

final DartVersion _dartVersion;

static const _code = LintCode(
name: 'no_force_unwrap',
problemMessage: 'Do not force unwrap',
url: 'https://github.com/dassssshers/nilts#no_force_unwrap',
);

@override
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
context.registry.addPostfixExpression((node) {
if (node.operator.type == TokenType.BANG) {
reporter.atToken(node.operator, _code);
}
});
}

@override
List<Fix> getFixes() => [
_AddNullCoalescingOperator(),
if (_dartVersion >= const DartVersion(major: 3, minor: 0, patch: 0))
_ReplaceWithPatternMatching(),
];
}

class _AddNullCoalescingOperator extends DartFix {
@override
void run(
CustomLintResolver resolver,
ChangeReporter reporter,
CustomLintContext context,
analyzer.AnalysisError analysisError,
List<analyzer.AnalysisError> others,
) {
context.registry.addPostfixExpression((node) {
if (!node.sourceRange.intersects(analysisError.sourceRange)) return;
if (node.operator.type != TokenType.BANG) return;

reporter
.createChangeBuilder(
message: 'Replace with null coalescing operator',
priority: ChangePriority.addNullCoalescingOperator,
)
.addDartFileEdit((builder) {
builder.addSimpleReplacement(
node.sourceRange,
'${node.operand} ?? /* Replace with a suitable default value */',
);
});
});
}
}

class _ReplaceWithPatternMatching extends DartFix {
@override
void run(
CustomLintResolver resolver,
ChangeReporter reporter,
CustomLintContext context,
analyzer.AnalysisError analysisError,
List<analyzer.AnalysisError> others,
) {
context.registry.addPostfixExpression((node) {
if (!node.sourceRange.intersects(analysisError.sourceRange)) return;
if (node.operator.type != TokenType.BANG) return;

reporter
.createChangeBuilder(
message: 'Replace with pattern matching',
priority: ChangePriority.addPatternMatching,
)
.addDartFileEdit((builder) {
builder.addSimpleReplacement(
node.sourceRange,
'if (${node.operand} case final value) return value',
);
});
});
}
}
6 changes: 6 additions & 0 deletions packages/nilts_test/test/lints/no_force_unwrap.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const String? nullableString = null;

void main() {
// expect_lint: no_force_unwrap
nullableString!.length;
}

0 comments on commit 61b896d

Please sign in to comment.