analysis_plugin_test_helper

Utilities to help write tests for the analyzer plugins.

Installation

From pub.dev

Add this to your pubspec.yaml

dependencies:
  analysis_plugin_test_helper: ^1.0.4

Or, From Git repo

dependencies:
  analysis_plugin_test_helper:
    git:
      url: https://github.com/Ragibn5/dart-flutter-packages.git
      path: analysis_plugin_test_helper
      ref: analysis_plugin_test_helper-1.0.4

For more information, see the package on pub.dev or the GitHub repository.

Getting Started

DartUnitResolver

Parsing Dart source (with parseString function from the analyzer package) gives you an unresolved AST, where every node in it is still just syntax. Whatever it refers to — a type, a declaration, a constant value — is only named, not linked to the actual thing. Without resolution, the AST tells you what's written, not what it means or what it points to.

Analyzer plugins work on the resolved picture — every reference linked to what it actually points to. Getting a resolved unit for a test normally means running the source through the full analysis pipeline by hand:

  • Create an AnalysisContextCollection
  • Write the source to a temp file
  • Resolve it and extract a ResolvedUnitResult
  • Clean up the temp files afterward

Every test file ends up repeating this. DartUnitResolver collapses it into one call — pass resolveSource() a Dart code string and get a resolved unit back.

import 'package:analysis_plugin_test_helper/analysis_plugin_test_helper.dart';
import 'package:test/test.dart';

void main() {
  final resolver = DartUnitResolver();

  setUpAll(() async => resolver.setUp());
  tearDownAll(() async => resolver.tearDown());

  test('example', () async {
    final result = await resolver.resolveSource('''
      class MyAnnotation {
        const MyAnnotation();
      }

      @MyAnnotation()
      class Foo {
        void myMethod() {}
      }
    ''');

    expect(result.diagnostics, isEmpty);
    // result.unit is a fully resolved CompilationUnit
  });
}

Note: The source must be standalone — every declaration it references (classes, annotations, imports) must be self-contained within the string, since there's no surrounding project context to resolve against.

resolveSource() doesn't throw or fail just because the source has errors — syntactically invalid or semantically incorrect code still comes back as a ResolvedUnitResult. If you need to confirm the source was actually valid, check result.diagnostics yourself. It only throws a StateError in the rarer case where the analyzer can't produce a ResolvedUnitResult at all.

Call setUp() once before your tests run and tearDown() once after. This only creates and cleans up the temp directory the source files are written to — each call to resolveSource() still spins up and disposes its own AnalysisContextCollection, so don't rely on any state carrying over between calls.

Parsers

Once you have result.unit from DartUnitResolver, these functions let you locate specific AST nodes without writing your own visitor.

Each capability comes in two forms:

Prefix Returns Behavior when not found
find* nullable value returns null
get* non-nullable value fails the current test via fail()

Use find* when you're asserting something is absent, and get* when you're asserting on something you expect to exist.

Annotations

Annotation? findAnnotation<D extends CompilationUnitMember>(
  CompilationUnit unit, {
  required String annotationName,
})

Matches on the annotation's resolved type — via elementAnnotation.computeConstantValue().type.element.name — not on its source text. Because of that, this only works on a resolved unit; against an unresolved one it always returns null. It also means the annotation doesn't have to spell the type out directly: @MyAnnotation(), a reference to a const variable of that type, and a call through a typedef alias of it all match annotationName: 'MyAnnotation'.

typedef MAN = MyAnnotation;

@MAN()
class Foo {}
final annotation = getAnnotation(result.unit, annotationName: 'MyAnnotation');

The search only checks metadata on the top-level declaration itself (unit.declarations) — annotations on members inside it, like a method or field, aren't inspected. By default it searches every top-level declaration kind (classes, top-level functions and getters, and so on); the type parameter D narrows that to one kind, e.g. getAnnotation<ClassDeclaration>(...), when you need to be specific about where the annotation should live.

Methods

MethodDeclaration? findMethodDeclaration(CompilationUnit unit, String name)

Searches the members of classes, mixins, extensions, extension types, and enums for a method named name. Top-level functions aren't covered.

final method = getMethodDeclaration(result.unit, 'myMethod');

Constructors

ConstructorDeclaration? findConstructorDeclaration(CompilationUnit unit, String? name)

Searches the same declaration kinds as findMethodDeclaration. name is the constructor's own name only — 'named', not 'Foo.named'. Pass null to find the unnamed (default) constructor.

This matches by name alone, so it finds factory constructors too — use findFactoryConstructorDeclaration below when you specifically need to assert that a constructor is (or isn't) a factory.

final defaultCtor = getConstructorDeclaration(result.unit, null);
final namedCtor = getConstructorDeclaration(result.unit, 'named');

Factory constructors

ConstructorDeclaration? findFactoryConstructorDeclaration(CompilationUnit unit, String? name)

Same lookup as findConstructorDeclaration, restricted to constructors declared with factory. null finds the default factory constructor.

final factoryCtor = getFactoryConstructorDeclaration(result.unit, 'create');

Import directives

ImportDirective? findImportDirective(CompilationUnit unit)

Returns the first import directive in the unit — there's no filtering by URI. Useful when a test source has a single import you want to assert on directly.

final importDirective = getImportDirective(result.unit);

Putting it together

test('plugin flags classes annotated with @MyAnnotation', () async {
  final result = await resolver.resolveSource('''
    class MyAnnotation {
      const MyAnnotation();
    }

    @MyAnnotation()
    class Foo {
      void myMethod() {}
    }
  ''');

  final annotation = getAnnotation(result.unit, annotationName: 'MyAnnotation');
  final method = getMethodDeclaration(result.unit, 'myMethod');

  expect(annotation, isNotNull);
  expect(method.name.lexeme, 'myMethod');
});

This is the shape most plugin tests take: resolve a small standalone snippet, pull the nodes under test out with the parser helpers, then assert on them directly rather than walking the AST by hand.

Libraries

analysis_plugin_test_helper
Utilities to help write tests for the analyzer plugins.