Skip to content

Commit eaedfb3

Browse files
committed
Add another rule: non_nullable_is_not_null_rule
1 parent 6cc66f1 commit eaedfb3

File tree

6 files changed

+235
-9
lines changed

6 files changed

+235
-9
lines changed

pkgs/test_analyzer_plugin/lib/main.dart

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import 'package:analysis_server_plugin/plugin.dart';
66
import 'package:analysis_server_plugin/registry.dart';
77

88
import 'src/fixes.dart';
9-
import 'src/rules.dart';
9+
import 'src/rules/non_nullable_is_not_null_rule.dart';
10+
import 'src/rules/test_in_test_rule.dart';
1011

1112
final plugin = TestPackagePlugin();
1213

@@ -16,5 +17,19 @@ class TestPackagePlugin extends Plugin {
1617
registry.registerWarningRule(TestInTestRule());
1718
registry.registerFixForRule(
1819
TestInTestRule.code, MoveBelowEnclosingTestCall.new);
20+
21+
registry.registerWarningRule(NonNullableIsNotNullRule());
22+
// Should we register a fix for this rule? The only automatic fix I can
23+
// think of would be to delete the entire statement:
24+
// `expect(a, isNotNull);` or `expect(a, isNull);`.
25+
26+
// TODO(srawlins): More rules to catch:
27+
// * `expect(7, contains(6))` - should only use `hasLength` with `Iterable`
28+
// or `String`.
29+
// * `expect(7, hasLength(3))` - should only use `hasLength` with `Iterable`
30+
// or `String`.
31+
// * `expect([].isEmpty, isFalse)` - should use `isEmpty` matcher.
32+
// * `expect([].isNotEmpty, isTrue)` - should use `isNotEmpty` matcher.
33+
// * `expect([].contains(7), isFalse)` - should use `contains` matcher.
1934
}
2035
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analyzer/analysis_rule/analysis_rule.dart';
6+
import 'package:analyzer/analysis_rule/rule_context.dart';
7+
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
8+
import 'package:analyzer/dart/ast/ast.dart';
9+
import 'package:analyzer/dart/ast/visitor.dart';
10+
import 'package:analyzer/dart/element/type_system.dart';
11+
import 'package:analyzer/error/error.dart';
12+
13+
import '../utilities.dart';
14+
15+
// TODO(srawlins): Several others; use same name or just different codes?
16+
// * `expect(null, isNotNull)` - always false
17+
// * `expect(null, isNull)` - always true
18+
// * `expect(7, isNull)` - always false
19+
class NonNullableIsNotNullRule extends MultiAnalysisRule {
20+
static const LintCode nonNullableIsNotNullCode = LintCode(
21+
'non_nullable_is_not_null',
22+
'Do not check whether a non-nullable value isNotNull',
23+
correctionMessage: 'Try changing the expectation, or removing it',
24+
);
25+
26+
static const LintCode nonNullableIsNullCode = LintCode(
27+
'non_nullable_is_null',
28+
'Do not check whether a non-nullable value isNull',
29+
correctionMessage: 'Try changing the expectation, or removing it',
30+
);
31+
32+
NonNullableIsNotNullRule()
33+
: super(
34+
name: 'non_nullable_is_not_null',
35+
description: "Non-nullable values will always pass an 'isNotNull' "
36+
"expectation and never pass an 'isNull' expectation.",
37+
);
38+
39+
@override
40+
List<LintCode> get diagnosticCodes => [nonNullableIsNotNullCode];
41+
42+
@override
43+
void registerNodeProcessors(
44+
RuleVisitorRegistry registry, RuleContext context) {
45+
var visitor = _Visitor(this, context.typeSystem);
46+
registry.addMethodInvocation(this, visitor);
47+
}
48+
}
49+
50+
class _Visitor extends SimpleAstVisitor<void> {
51+
final MultiAnalysisRule rule;
52+
53+
final TypeSystem typeSystem;
54+
55+
_Visitor(this.rule, this.typeSystem);
56+
57+
@override
58+
void visitMethodInvocation(MethodInvocation node) {
59+
if (!node.methodName.isExpect) {
60+
return;
61+
}
62+
63+
if (node.argumentList.arguments
64+
case [var actual, SimpleIdentifier matcher]) {
65+
var actualType = actual.staticType;
66+
if (actualType == null) return;
67+
if (typeSystem.isNonNullable(actualType)) {
68+
if (matcher.isNotNull) {
69+
// The actual value will always match this matcher.
70+
rule.reportAtNode(matcher,
71+
diagnosticCode:
72+
NonNullableIsNotNullRule.nonNullableIsNotNullCode);
73+
} else if (matcher.isNull) {
74+
// The actual value will never match this matcher.
75+
rule.reportAtNode(matcher,
76+
diagnosticCode: NonNullableIsNotNullRule.nonNullableIsNullCode);
77+
}
78+
}
79+
}
80+
}
81+
}

pkgs/test_analyzer_plugin/lib/src/rules.dart renamed to pkgs/test_analyzer_plugin/lib/src/rules/test_in_test_rule.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
44

5+
import 'package:analyzer/analysis_rule/analysis_rule.dart';
6+
import 'package:analyzer/analysis_rule/rule_context.dart';
7+
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
58
import 'package:analyzer/dart/ast/ast.dart';
69
import 'package:analyzer/dart/ast/visitor.dart';
7-
import 'package:analyzer/src/dart/error/lint_codes.dart';
8-
import 'package:analyzer/src/lint/linter.dart';
10+
import 'package:analyzer/error/error.dart';
911

10-
import 'utilities.dart';
12+
import '../utilities.dart';
1113

1214
class TestInTestRule extends AnalysisRule {
1315
static const LintCode code = LintCode(
@@ -25,11 +27,11 @@ class TestInTestRule extends AnalysisRule {
2527
);
2628

2729
@override
28-
LintCode get lintCode => code;
30+
LintCode get diagnosticCode => code;
2931

3032
@override
3133
void registerNodeProcessors(
32-
NodeLintRegistry registry, LinterContext context) {
34+
RuleVisitorRegistry registry, RuleContext context) {
3335
var visitor = _Visitor(this);
3436
registry.addMethodInvocation(this, visitor);
3537
}
@@ -47,7 +49,7 @@ class _Visitor extends SimpleAstVisitor<void> {
4749
}
4850
var enclosingTestCall = findEnclosingTestCall(node);
4951
if (enclosingTestCall != null) {
50-
rule.reportLint(node);
52+
rule.reportAtNode(node);
5153
}
5254
}
5355
}

pkgs/test_analyzer_plugin/lib/src/utilities.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ extension SimpleIdentifierExtension on SimpleIdentifier {
2323
final element = this.element;
2424
if (element == null) return false;
2525
if (element.name3 != 'test') return false;
26-
return element.library2?.uri.path.startsWith('test_core/') ?? false;
26+
return element.library?.uri.path.startsWith('test_core/') ?? false;
2727
}
2828

2929
/// Whether this identifier represents the 'group' function from the
@@ -32,6 +32,33 @@ extension SimpleIdentifierExtension on SimpleIdentifier {
3232
final element = this.element;
3333
if (element == null) return false;
3434
if (element.name3 != 'group') return false;
35-
return element.library2?.uri.path.startsWith('test_core/') ?? false;
35+
return element.library?.uri.path.startsWith('test_core/') ?? false;
36+
}
37+
38+
/// Whether this identifier represents the 'expect' function from the
39+
/// 'matcher' package.
40+
bool get isExpect {
41+
final element = this.element;
42+
if (element == null) return false;
43+
if (element.name3 != 'expect') return false;
44+
return element.library?.uri.path.startsWith('matcher/') ?? false;
45+
}
46+
47+
/// Whether this identifier represents the 'isNotNull' constant from the
48+
/// 'matcher' package.
49+
bool get isNotNull {
50+
final element = this.element;
51+
if (element == null) return false;
52+
if (element.name3 != 'isNotNull') return false;
53+
return element.library?.uri.path.startsWith('matcher/') ?? false;
54+
}
55+
56+
/// Whether this identifier represents the 'isNull' constant from the
57+
/// 'matcher' package.
58+
bool get isNull {
59+
final element = this.element;
60+
if (element == null) return false;
61+
if (element.name3 != 'isNull') return false;
62+
return element.library?.uri.path.startsWith('matcher/') ?? false;
3663
}
3764
}

pkgs/test_analyzer_plugin/pubspec.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,21 @@ environment:
99
dependencies:
1010
analysis_server_plugin: any
1111
analyzer: ^7.2.0
12+
analyzer_plugin:
13+
analyzer_testing:
1214

15+
dev_dependencies:
16+
test: any
17+
test_reflective_loader: any
18+
19+
dependency_overrides:
20+
_fe_analyzer_shared:
21+
path: /Users/srawlins/code/dart-sdk/sdk/pkg/_fe_analyzer_shared
22+
analysis_server_plugin:
23+
path: /Users/srawlins/code/dart-sdk/sdk/pkg/analysis_server_plugin
24+
analyzer:
25+
path: /Users/srawlins/code/dart-sdk/sdk/pkg/analyzer
26+
analyzer_plugin:
27+
path: /Users/srawlins/code/dart-sdk/sdk/pkg/analyzer_plugin
28+
analyzer_testing:
29+
path: /Users/srawlins/code/dart-sdk/sdk/pkg/analyzer_testing
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// ignore_for_file: non_constant_identifier_names
6+
7+
import 'package:analyzer/src/lint/registry.dart';
8+
import 'package:analyzer/utilities/package_config_file_builder.dart';
9+
import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
10+
import 'package:test_analyzer_plugin/src/rules/non_nullable_is_not_null_rule.dart';
11+
import 'package:test_reflective_loader/test_reflective_loader.dart';
12+
13+
void main() {
14+
defineReflectiveSuite(() {
15+
defineReflectiveTests(NonNullableIsNotNullTest);
16+
});
17+
}
18+
19+
@reflectiveTest
20+
class NonNullableIsNotNullTest extends AnalysisRuleTest {
21+
@override
22+
void setUp() {
23+
Registry.ruleRegistry.registerLintRule(NonNullableIsNotNullRule());
24+
25+
super.setUp();
26+
27+
var matcherPath = '/packages/matcher';
28+
newFile('$matcherPath/lib/matcher.dart', '''
29+
void expect(dynamic actual, dynamic matcher) {}
30+
31+
const isNotNull = 0;
32+
const isNull = 0;
33+
''');
34+
writeTestPackageConfig(
35+
PackageConfigFileBuilder()
36+
..add(name: 'matcher', rootPath: convertPath(matcherPath)),
37+
);
38+
}
39+
40+
@override
41+
String get analysisRule => 'non_nullable_is_not_null';
42+
43+
void test_nullableValue_isNotNullMatcher() async {
44+
await assertNoDiagnostics(r'''
45+
import 'package:matcher/matcher.dart';
46+
void f(String? p) {
47+
expect(p, isNotNull);
48+
}
49+
''');
50+
}
51+
52+
void test_nonNullableValue_isNotNullMatcher() async {
53+
await assertDiagnostics(
54+
r'''
55+
import 'package:matcher/matcher.dart';
56+
void f() {
57+
expect(123, isNotNull);
58+
}
59+
''',
60+
[lint(64, 9)],
61+
);
62+
}
63+
64+
void test_nullableValue_isNullMatcher() async {
65+
await assertNoDiagnostics(r'''
66+
import 'package:matcher/matcher.dart';
67+
void f(String? p) {
68+
expect(p, isNull);
69+
}
70+
''');
71+
}
72+
73+
void test_nonNullableValue_isNullMatcher() async {
74+
await assertDiagnostics(
75+
r'''
76+
import 'package:matcher/matcher.dart';
77+
void f() {
78+
expect(123, isNull);
79+
}
80+
''',
81+
[lint(64, 6, name: 'non_nullable_is_null')],
82+
);
83+
}
84+
}

0 commit comments

Comments
 (0)