diff --git a/lib/main.dart b/lib/main.dart index 707269e3..4b1be247 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/registry.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; +import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; /// The entry point for the Solid Lints analyser server plugin. /// @@ -21,5 +22,8 @@ class SolidLintsPlugin extends Plugin { registry.registerLintRule( AvoidGlobalStateRule(), ); + registry.registerLintRule( + AvoidNonNullAssertionRule(), + ); } } diff --git a/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart b/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart index 91a38cc4..6ee78241 100644 --- a/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart +++ b/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart @@ -39,7 +39,7 @@ class AvoidGlobalStateRule extends AnalysisRule { static const String lintName = 'avoid_global_state'; /// Lint code used for suppression and reporting. - static const LintCode code = LintCode( + static const LintCode _code = LintCode( lintName, 'Avoid variables that can be globally mutated.', correctionMessage: @@ -54,7 +54,7 @@ class AvoidGlobalStateRule extends AnalysisRule { ); @override - LintCode get diagnosticCode => code; + LintCode get diagnosticCode => _code; @override void registerNodeProcessors( 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 2b27901d..ba98b75a 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,10 +1,8 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/token.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; +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/visitors/avoid_non_null_assertion_visitor.dart'; /// Rule which warns about usages of bang operator ("!") /// as it may result in unexpected runtime exceptions. @@ -37,56 +35,32 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// final map = {'key': 'value'}; /// map['key']!; /// ``` -class AvoidNonNullAssertionRule extends SolidLintRule { - /// This lint rule represents - /// the error whether we use bang operator. - static const lintName = 'avoid_non_null_assertion'; +class AvoidNonNullAssertionRule extends AnalysisRule { + /// Name of the lint + static const String lintName = 'avoid_non_null_assertion'; - AvoidNonNullAssertionRule._(super.config); + /// Lint code used for suppression and reporting + static const LintCode _code = LintCode( + lintName, + 'Avoid using the bang operator. It may result in runtime exceptions.', + ); - /// Creates a new instance of [AvoidNonNullAssertionRule] - /// based on the lint configuration. - factory AvoidNonNullAssertionRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (_) => 'Avoid using the bang operator. ' - 'It may result in runtime exceptions.', - ); + /// creates an instance of [AvoidNonNullAssertionRule] + AvoidNonNullAssertionRule() + : super( + name: lintName, + description: + 'Warns about usages of bang operator (!) except valid Map access.', + ); - return AvoidNonNullAssertionRule._(rule); - } + @override + LintCode get diagnosticCode => _code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addPostfixExpression((node) { - if (node.operator.type != TokenType.BANG) { - return; - } - - // DCM's and Flutter's documentation treats "bang" as a valid way of - // accessing a Map. For compatibility it's excluded from this rule. - // See more: - // * https://dcm.dev/docs/rules/common/avoid-non-null-assertion - // * https://dart.dev/null-safety/understanding-null-safety#the-map-index-operator-is-nullable - final operand = node.operand; - if (operand is IndexExpression) { - final type = operand.target?.staticType; - final isInterface = type is InterfaceType; - final isMap = isInterface && - (type.isDartCoreMap || - type.allSupertypes.any((v) => v.isDartCoreMap)); - - if (isMap) { - return; - } - } - - reporter.atNode(node, code); - }); + registry.addPostfixExpression(this, AvoidNonNullAssertionVisitor(this)); } } 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 new file mode 100644 index 00000000..1dd32ae2 --- /dev/null +++ b/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart @@ -0,0 +1,41 @@ +import 'package:analyzer/dart/ast/ast.dart'; +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'; + +/// visitor for [AvoidNonNullAssertionRule] +class AvoidNonNullAssertionVisitor extends SimpleAstVisitor { + /// Rule associated with this visitor + final AvoidNonNullAssertionRule rule; + + /// Creates an instance of [AvoidNonNullAssertionVisitor] + AvoidNonNullAssertionVisitor(this.rule); + + @override + void visitPostfixExpression(PostfixExpression node) { + if (node.operator.type != TokenType.BANG) { + return; + } + + final operand = node.operand; + + if (operand is IndexExpression) { + final type = operand.target?.staticType; + + if (_isMap(type)) { + return; + } + } + + rule.reportAtNode(node); + } + + bool _isMap(DartType? type) { + if (type is! InterfaceType) { + return false; + } + + return type.isDartCoreMap || type.allSupertypes.any((v) => v.isDartCoreMap); + } +} diff --git a/lint_test/avoid_non_null_assertion_test.dart b/lint_test/avoid_non_null_assertion_test.dart deleted file mode 100644 index 2a33a7df..00000000 --- a/lint_test/avoid_non_null_assertion_test.dart +++ /dev/null @@ -1,25 +0,0 @@ -// ignore_for_file: avoid_global_state, prefer_match_file_name -// ignore_for_file: member_ordering - -/// Check "bang" operator fail -/// -/// `avoid_non_null_assertion` -class AvoidNonNullAssertion { - AvoidNonNullAssertion? object; - int? number; - - void test() { - // expect_lint: avoid_non_null_assertion - number!; - - // expect_lint: avoid_non_null_assertion - object!.number!; - - // expect_lint: avoid_non_null_assertion - object!.test(); - - // No lint on maps - final map = {'key': 'value'}; - map['key']!; - } -} diff --git a/test/avoid_non_null_assertion_rule_test.dart b/test/avoid_non_null_assertion_rule_test.dart new file mode 100644 index 00000000..8d054cda --- /dev/null +++ b/test/avoid_non_null_assertion_rule_test.dart @@ -0,0 +1,59 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.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'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidNonNullAssertionRuleTest); + }); +} + +@reflectiveTest +class AvoidNonNullAssertionRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidNonNullAssertionRule(); + super.setUp(); + } + + void test_reports_non_null_assertion_on_nullable_value() async { + await assertDiagnostics( + r''' +void m(int? number) { + final value = number!; +} +''', + [lint(38, 7)], + ); + } + + void test_reports_non_null_assertion_on_method_call() async { + await assertDiagnostics( + r''' +void m(Object? object) { + object!.toString(); +} +''', + [lint(27, 7)], + ); + } + + void test_does_not_report_map_access() async { + await assertNoDiagnostics(r''' +void m() { + final map = {'key': 'value'}; + map['key']!; +} +'''); + } + + void test_does_not_report_safe_null_check() async { + await assertNoDiagnostics(r''' +void m(int? number) { + if (number != null) { + final value = number; + } +} +'''); + } +}