Skip to content

Commit e04eec7

Browse files
committed
Introduce an analyzer plugin for the test package.
1 parent 1a6c5ac commit e04eec7

File tree

6 files changed

+193
-0
lines changed

6 files changed

+193
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# test_analyzer_plugin
2+
3+
This package is an analyzer plugin that provides additional static analysis for
4+
usage of the test package.
5+
6+
This analyzer plugin provides the following additional analysis:
7+
8+
* Report a warning when a `test` or a `group` is declared inside a `test`
9+
declaration. This can _sometimes_ be detected at runtime. This warning is
10+
reported statically.
11+
12+
* Offer a quick fix in the IDE for the above warning, which moves the violating
13+
`test` or `group` declaration below the containing `test` declaration.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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:analysis_server_plugin/plugin.dart';
6+
import 'package:analysis_server_plugin/registry.dart';
7+
8+
import 'src/fixes.dart';
9+
import 'src/rules.dart';
10+
11+
final plugin = TestPackagePlugin();
12+
13+
class TestPackagePlugin extends Plugin {
14+
@override
15+
void register(PluginRegistry registry) {
16+
registry.registerWarningRule(TestInTestRule());
17+
registry.registerFixForRule(
18+
TestInTestRule.code, MoveBelowEnclosingTestCall.new);
19+
}
20+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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:analysis_server_plugin/edit/dart/correction_producer.dart';
6+
import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart';
7+
import 'package:analyzer/dart/ast/ast.dart';
8+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
9+
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
10+
import 'package:analyzer_plugin/utilities/range_factory.dart';
11+
12+
import 'utilities.dart';
13+
14+
class MoveBelowEnclosingTestCall extends ResolvedCorrectionProducer {
15+
static const _wrapInQuotesKind = FixKind(
16+
'dart.fix.moveBelowEnclosingTestCall',
17+
DartFixKindPriority.standard,
18+
"Move below the enclosing 'test' call");
19+
20+
MoveBelowEnclosingTestCall({required super.context});
21+
22+
@override
23+
CorrectionApplicability get applicability =>
24+
// This fix may break code by moving references to variables away from the
25+
// scope in which they are declared.
26+
CorrectionApplicability.singleLocation;
27+
28+
@override
29+
FixKind get fixKind => _wrapInQuotesKind;
30+
31+
@override
32+
Future<void> compute(ChangeBuilder builder) async {
33+
var methodCall = node;
34+
if (methodCall is! MethodInvocation) return;
35+
AstNode? enclosingTestCall = findEnclosingTestCall(methodCall);
36+
if (enclosingTestCall == null) return;
37+
38+
if (enclosingTestCall.parent is ExpressionStatement) {
39+
// Move the 'test' call to below the outer 'test' call _statement_.
40+
enclosingTestCall = enclosingTestCall.parent!;
41+
}
42+
43+
if (methodCall.parent is ExpressionStatement) {
44+
// Move the whole statement (don't leave the semicolon dangling).
45+
methodCall = methodCall.parent!;
46+
}
47+
48+
await builder.addDartFileEdit(file, (builder) {
49+
var indent = utils.getLinePrefix(enclosingTestCall!.offset);
50+
var source = utils.getRangeText(range.node(methodCall));
51+
52+
// Move the source for `methodCall` wholsale to be just after `enclosingTestCall`.
53+
builder.addDeletion(range.deletionRange(methodCall));
54+
builder.addSimpleInsertion(
55+
enclosingTestCall.end, '$eol$eol$indent$source');
56+
});
57+
}
58+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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/dart/ast/ast.dart';
6+
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';
9+
10+
import 'utilities.dart';
11+
12+
class TestInTestRule extends AnalysisRule {
13+
static const LintCode code = LintCode(
14+
'test_in_test',
15+
"Do not declare a 'test' or a 'group' inside a 'test'",
16+
correctionMessage: "Try moving 'test' or 'group' outside of 'test'",
17+
);
18+
19+
TestInTestRule()
20+
: super(
21+
name: 'test_in_test',
22+
description:
23+
'Tests and groups declared inside of a test are not properly '
24+
'registered in the test framework.',
25+
);
26+
27+
@override
28+
LintCode get lintCode => code;
29+
30+
@override
31+
void registerNodeProcessors(
32+
NodeLintRegistry registry, LinterContext context) {
33+
var visitor = _Visitor(this);
34+
registry.addMethodInvocation(this, visitor);
35+
}
36+
}
37+
38+
class _Visitor extends SimpleAstVisitor<void> {
39+
final AnalysisRule rule;
40+
41+
_Visitor(this.rule);
42+
43+
@override
44+
void visitMethodInvocation(MethodInvocation node) {
45+
if (!node.methodName.isTest && !node.methodName.isGroup) {
46+
return;
47+
}
48+
var enclosingTestCall = findEnclosingTestCall(node);
49+
if (enclosingTestCall != null) {
50+
rule.reportLint(node);
51+
}
52+
}
53+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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/dart/ast/ast.dart';
6+
7+
/// Finds an enclosing call to the 'test' function, if there is one.
8+
MethodInvocation? findEnclosingTestCall(MethodInvocation node) {
9+
var ancestor = node.parent?.thisOrAncestorOfType<MethodInvocation>();
10+
while (ancestor != null) {
11+
if (ancestor.methodName.isTest) {
12+
return ancestor;
13+
}
14+
ancestor = ancestor.parent?.thisOrAncestorOfType<MethodInvocation>();
15+
}
16+
return null;
17+
}
18+
19+
extension SimpleIdentifierExtension on SimpleIdentifier {
20+
/// Whether this identifier represents the 'test' function from the
21+
/// 'test_core' package.
22+
bool get isTest {
23+
final element = this.element;
24+
if (element == null) return false;
25+
if (element.name3 != 'test') return false;
26+
return element.library2?.uri.path.startsWith('test_core/') ?? false;
27+
}
28+
29+
/// Whether this identifier represents the 'group' function from the
30+
/// 'test_core' package.
31+
bool get isGroup {
32+
final element = this.element;
33+
if (element == null) return false;
34+
if (element.name3 != 'group') return false;
35+
return element.library2?.uri.path.startsWith('test_core/') ?? false;
36+
}
37+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: test_analyzer_plugin
2+
description: An analyzer plugin to report improper usage of the test package.
3+
version: 1.0.0
4+
publish_to: none
5+
6+
environment:
7+
sdk: '>=3.6.0 <4.0.0'
8+
9+
dependencies:
10+
analysis_server_plugin: any
11+
analyzer: ^7.2.0
12+

0 commit comments

Comments
 (0)