Skip to content

Commit

Permalink
Introduce an analyzer plugin for the test package.
Browse files Browse the repository at this point in the history
  • Loading branch information
srawlins committed Feb 25, 2025
1 parent 17609bf commit 5c9e852
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 0 deletions.
13 changes: 13 additions & 0 deletions pkgs/test_analyzer_plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# test_analyzer_plugin

This package is an analyzer plugin that provides additional static analysis for
usage of the test package.

This analyzer plugin provides the following additional analysis:

* Report a warning when a `test` or a `group` is declared inside a `test`
declaration. This can _sometimes_ be detected at runtime. This warning is
reported statically.

* Offer a quick fix in the IDE for the above warning, which moves the violating
`test` or `group` declaration below the containing `test` declaration.
20 changes: 20 additions & 0 deletions pkgs/test_analyzer_plugin/lib/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analysis_server_plugin/plugin.dart';
import 'package:analysis_server_plugin/registry.dart';

import 'src/fixes.dart';
import 'src/rules.dart';

final plugin = TestPackagePlugin();

class TestPackagePlugin extends Plugin {
@override
void register(PluginRegistry registry) {
registry.registerWarningRule(TestInTestRule());
registry.registerFixForRule(
TestInTestRule.code, MoveBelowEnclosingTestCall.new);
}
}
58 changes: 58 additions & 0 deletions pkgs/test_analyzer_plugin/lib/src/fixes.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';

import 'utilities.dart';

class MoveBelowEnclosingTestCall extends ResolvedCorrectionProducer {
static const _wrapInQuotesKind = FixKind(
'dart.fix.moveBelowEnclosingTestCall',
DartFixKindPriority.standard,
"Move below the enclosing 'test' call");

MoveBelowEnclosingTestCall({required super.context});

@override
CorrectionApplicability get applicability =>
// This fix may break code by moving references to variables away from the
// scope in which they are declared.
CorrectionApplicability.singleLocation;

@override
FixKind get fixKind => _wrapInQuotesKind;

@override
Future<void> compute(ChangeBuilder builder) async {
var methodCall = node;
if (methodCall is! MethodInvocation) return;
AstNode? enclosingTestCall = findEnclosingTestCall(methodCall);
if (enclosingTestCall == null) return;

if (enclosingTestCall.parent is ExpressionStatement) {
// Move the 'test' call to below the outer 'test' call _statement_.
enclosingTestCall = enclosingTestCall.parent!;
}

if (methodCall.parent is ExpressionStatement) {
// Move the whole statement (don't leave the semicolon dangling).
methodCall = methodCall.parent!;
}

await builder.addDartFileEdit(file, (builder) {
var indent = utils.getLinePrefix(enclosingTestCall!.offset);
var source = utils.getRangeText(range.node(methodCall));

// Move the source for `methodCall` wholsale to be just after `enclosingTestCall`.
builder.addDeletion(range.deletionRange(methodCall));
builder.addSimpleInsertion(
enclosingTestCall.end, '$eol$eol$indent$source');
});
}
}
53 changes: 53 additions & 0 deletions pkgs/test_analyzer_plugin/lib/src/rules.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/src/dart/error/lint_codes.dart';
import 'package:analyzer/src/lint/linter.dart';

import 'utilities.dart';

class TestInTestRule extends AnalysisRule {
static const LintCode code = LintCode(
'test_in_test',
"Do not declare a 'test' or a 'group' inside a 'test'",
correctionMessage: "Try moving 'test' or 'group' outside of 'test'",
);

TestInTestRule()
: super(
name: 'test_in_test',
description:
'Tests and groups declared inside of a test are not properly '
'registered in the test framework.',
);

@override
LintCode get lintCode => code;

@override
void registerNodeProcessors(
NodeLintRegistry registry, LinterContext context) {
var visitor = _Visitor(this);
registry.addMethodInvocation(this, visitor);
}
}

class _Visitor extends SimpleAstVisitor<void> {
final AnalysisRule rule;

_Visitor(this.rule);

@override
void visitMethodInvocation(MethodInvocation node) {
if (!node.methodName.isTest && !node.methodName.isGroup) {
return;
}
var enclosingTestCall = findEnclosingTestCall(node);
if (enclosingTestCall != null) {
rule.reportLint(node);
}
}
}
37 changes: 37 additions & 0 deletions pkgs/test_analyzer_plugin/lib/src/utilities.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analyzer/dart/ast/ast.dart';

/// Finds an enclosing call to the 'test' function, if there is one.
MethodInvocation? findEnclosingTestCall(MethodInvocation node) {
var ancestor = node.parent?.thisOrAncestorOfType<MethodInvocation>();
while (ancestor != null) {
if (ancestor.methodName.isTest) {
return ancestor;
}
ancestor = ancestor.parent?.thisOrAncestorOfType<MethodInvocation>();
}
return null;
}

extension SimpleIdentifierExtension on SimpleIdentifier {
/// Whether this identifier represents the 'test' function from the
/// 'test_core' package.
bool get isTest {
final element = this.element;
if (element == null) return false;
if (element.name3 != 'test') return false;
return element.library2?.uri.path.startsWith('test_core/') ?? false;
}

/// Whether this identifier represents the 'group' function from the
/// 'test_core' package.
bool get isGroup {
final element = this.element;
if (element == null) return false;
if (element.name3 != 'group') return false;
return element.library2?.uri.path.startsWith('test_core/') ?? false;
}
}
12 changes: 12 additions & 0 deletions pkgs/test_analyzer_plugin/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: test_analyzer_plugin
description: An analyzer plugin to report improper usage of the test package.
version: 1.0.0
publish_to: none

environment:
sdk: '>=3.6.0 <4.0.0'

dependencies:
analysis_server_plugin: any
analyzer: ^7.2.0

0 comments on commit 5c9e852

Please sign in to comment.