この記事は CyberAgent Developers Advent Calendar 2025 の10日目の記事です。

こんにちは、FANTECH本部 Fanbase事業部でモバイルアプリエンジニアをしている成尾 嘉貴(@naruogram)です。

本記事では、Dart 3.10で追加されたAnalyzer Pluginsを使って独自のLintルールを作る方法を、静的解析の仕組みから順を追って解説します。

これまでDartで独自のLintルールを作成するには custom_lint パッケージを使用するのが一般的でした。しかし、Dart 3.10でAnalyzer Pluginsが公式にサポートされたことで、サードパーティのパッケージに頼らずにカスタムルールを実装できるようになりました。

目次

  1. Analyzer Pluginsの概要
  2. 静的解析の仕組み:ASTとVisitorパターン
  3. Analysis Serverの仕組みと制約
  4. Lintルールを実装する
  5. Quick Fixを実装する
  6. Lintルールをテストする
  7. custom_lintとの比較

Analyzer Pluginsの概要

Analyzer Pluginsは、Dart Analyzerを拡張するための仕組みです。以下の機能を追加できます。

  • Analysis Rules: IDEとコマンドラインで診断メッセージを表示
  • Quick Fixes: 診断に対応した修正を提案
  • Quick Assists: 特定の構文に対するリファクタリングを提供

作成したプラグインはIDEで直接動作し、dart analyzeflutter analyze からも利用できます。

静的解析の仕組み:ASTとVisitorパターン

AST

静的解析ツールは、ソースコードをAST(Abstract Syntax Tree)という木構造に変換して解析します。ソースコードの各要素(関数宣言、メソッド呼び出し、変数など)が親子関係を持つノードとして表現されます。

void main() {
  print('Hello');
}

このコードは以下のような構造になります。

CompilationUnit(1つのDartファイル全体を表すルートノード)
└── FunctionDeclaration (main関数の宣言)
    └── Block(`{ }` で囲まれたコードブロック)
        └── ExpressionStatement(式で構成される文)
            └── MethodInvocation (`print()` メソッドの呼び出し)
                └── SimpleStringLiteral (`'Hello'` という文字列リテラル)

Lintルールを作るとは、このASTを走査して「問題のあるパターン」を見つけることになります。

Visitorパターン

ASTを走査する方法として、Dart AnalyzerではVisitorパターンを採用しています。ASTは深くネストする可能性があるため、単純なループでは対応が難しいですが、Visitorパターンなら、処理対象のノードタイプのメソッドだけをオーバーライドすれば済みます。

今回のAnalyzer PluginsでのLintルールではSimpleAstVisitorを継承し、visitMethodInvocation メソッドだけをオーバーライドしてメソッド呼び出しをチェックします。

class MyVisitor extends SimpleAstVisitor<void> {
  @override
  void visitMethodInvocation(MethodInvocation node) {
    // メソッド呼び出しだけをチェック
  }
}

Analysis Serverの仕組みと制約

Analyzer Pluginsは、Analysis Server内の別Isolateで実行されます。Isolateは独立したメモリ空間を持つため、プラグインがクラッシュしてもAnalysis Server本体には影響しません。

この設計により以下の制約があります。

  • print() デバッグ不可: 標準出力がコンソールに接続されていない
  • 再起動が必須: プラグインは起動時に一度だけ読み込まれるため、コード変更後はAnalysis Serverの再起動が必要

Lintルールを実装する

必要な環境

Step 1: プラグインの基本構造

lib/main.dartを作成します。Analysis Serverはこのファイルのトップレベル plugin変数を参照します。

Analysis Serverは、プラグインを使用する際に以下のような流れで動作します。
analysis_options.yamlに記載されたプラグインを発見すると、そのプラグインを読み込むためのコードを自動的に生成します。このコードは、プラグインの lib/main.dart ファイルをインポートし、そこで定義されている plugin 変数を直接参照してプラグインのインスタンスを取得します。インスタンスが取得できたら、Analysis Server は register() メソッドを呼び出し、プラグインが提供するルールやFix、Assistを登録します。これにより、プラグインがIDEやコマンドラインツールで利用可能になります。

そのため、下記のようにlib/main.dart を作成し、Analysis Server が読み込めるように top-level の plugin 変数を定義する必要があります。

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

final plugin = MyCustomPlugin();

class MyCustomPlugin extends Plugin {
  @override
  String get name => 'My Custom Lint Plugin';

  @override
  void register(PluginRegistry registry) {
    // ルールを登録
  }
}

Step 2: ルールの実装

今回は例として「print()の使用を禁止する」というLintルールを作成します。
ルールはRule classVisitor classの2つで構成されます。

この設計により、「何をチェックするか(ルールの定義)」と「どうチェックするか(チェックロジック)」が明確に分離されます。

Rule classの役割

AnalysisRuleを継承したクラスで、ルール自体のメタデータと設定を保持します。単一の診断コードを扱うルールに特化しており、reportAtNode() などのヘルパーメソッドで簡潔にエラー報告ができます。

registerNodeProcessors では、チェック対象のノードタイプを登録します。addMethodInvocation で登録すると、メソッド呼び出しノードだけが効率的にVisitorに渡されます。

class AvoidPrintRule extends AnalysisRule {
  static const LintCode code = LintCode(
    'avoid_print_in_code',                     // ルールID
    "Avoid using 'print' in production code.", // 警告メッセージ
    correctionMessage: "Consider using 'debugPrint'.",
  );

  AvoidPrintRule()
      : super(
          name: 'avoid_print_in_code',
          description: 'Prevents the use of print().',
        );

  @override
  LintCode get diagnosticCode => code;

  @override
  void registerNodeProcessors(
    RuleVisitorRegistry registry,
    RuleContext context,
  ) {
    registry.addMethodInvocation(this, _AvoidPrintVisitor(this));
  }
}

Visitor classの役割

SimpleAstVisitorを継承したクラスで、実際のチェックロジックを実装します。

class _AvoidPrintVisitor extends SimpleAstVisitor {
  final AvoidPrintRule rule;

  _AvoidPrintVisitor(this.rule);

  @override
  void visitMethodInvocation(MethodInvocation node) {
    if (node.methodName.name == 'print') {
      final element = node.methodName.staticElement;
      if (element is ExecutableElement &&
          element.library?.name == 'dart.core') {
        rule.reportAtNode(node.methodName);
      }
    }
    super.visitMethodInvocation(node);
  }
}

処理の流れ

Analysis Serverがファイルを解析開始すると、まず AvoidPrintRule.registerNodeProcessors() が呼ばれます。このメソッド内で _AvoidPrintVisitor インスタンスを作成し、registry.addMethodInvocation(this, visitor) でビジターを登録します。

登録後、コード内のすべての MethodInvocation(メソッド呼び出し)ノードに対して、ビジターの visitMethodInvocation() が自動的に呼び出されます。このメソッド内で print() を発見したら、rule.reportAtNode() で診断を報告し、IDEやコマンドライン(dart analyze)に警告が表示されます。

Step 3: ルールの登録

@override
void register(PluginRegistry registry) {
  registry.registerLintRule(AvoidPrintRule());
}

ルールの登録には2つの方法があります。

registerLintRule() で登録すると、analysis_options.yamlで明示的に有効化しない限り動作しません。これは、コーディングスタイルに関するルールなど、プロジェクトごとに選択的に使いたい場合に適しています。

一方、registerWarningRule() で登録すると、デフォルトで有効になります。

Step 4: プラグインの有効化

analysis_options.yamlを作成します。これで実際にanalyzer pluginsで独自Lintを作成することができました。

plugins:
  my_custom_lint:
    path: path/my_custom_lint
    diagnostics:
      avoid_print_in_code: true # 先ほど例で作成したlintルール

Quick Fixを実装する

診断を報告するだけでなく、Quick Fix(自動修正機能)を提供できます。Quick Fixは診断(Lint警告)とセットで動作します。IDEで診断の上に表示されるようになり、コードを自動的に修正します。

Quick Fixの実装

Quick Fixは ResolvedCorrectionProducer を継承して実装します。compute メソッド内で ChangeBuilder を使用してコード変更を記述します。

print() を削除するQuick Fixを実装してみましょう。

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/source/source_range.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';

class RemovePrintFix extends ResolvedCorrectionProducer {
  static const _fixKind = FixKind(
    'dart.fix.remove_print', // Fix の識別子
    DartFixKindPriority.standard, // 優先度
    'Remove print statement', // IDE に表示されるメッセージ
  );

  RemovePrintFix({required super.context});

  @override
  CorrectionApplicability get applicability =>
      CorrectionApplicability.singleLocation;

  @override
  FixKind get fixKind => _fixKind;

  @override
  Future<void> compute(ChangeBuilder builder) async {
    final node = this.node;

    // print() を含む式文全体を見つける
    AstNode? targetNode = node;
    while (targetNode != null && targetNode is! ExpressionStatement) {
      targetNode = targetNode.parent;
    }

    if (targetNode case final targetNode?) {
      await builder.addDartFileEdit(file, (builder) {
        final range = SourceRange(targetNode.offset, targetNode.length);
        builder.addDeletion(range);
      });
    }
  }
}

ResolvedCorrectionProducerはQuick Fixの基底クラスで、解決済みのAST情報にアクセスできます。各FixはFixKindを通じて識別子、優先度、表示メッセージといったメタデータを定義します。実際のコード変更はChangeBuilderを使って記述します。addDartFileEditでDartファイルへの編集を追加し、addDeletionで指定範囲のコードを削除するといった操作が可能です。Fixを複数箇所に一括適用したい場合は、CorrectionApplicabilityで適用範囲を指定できます。

Fixの登録

lib/main.dartで registerFixForRule を使用して、Fixを特定の診断コードに関連付けて登録します。

@override
void register(PluginRegistry registry) {
  registry.registerLintRule(AvoidPrintRule());
  registry.registerFixForRule(
    AvoidPrintRule.code,
    ({required context}) => RemovePrintFix(context: context),
  );
}

これで、IDEで print() に警告が表示され、Quick Fixが使えるようになりました。

Lintルールをテストする

パッケージ: test, test_reflective_loader, analyzer_testing
analyzer_testingのパッケージを使用してルールをテストできます。test_reflective_loaderのパッケージを使用したクラスベースの構造でテストを記述します。

import 'package:analyzer/src/lint/registry.dart';
import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
import 'package:my_custom_lint/rules/avoid_print_rule.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

void main() {
  defineReflectiveSuite(() {
    defineReflectiveTests(AvoidPrintRuleTest);
  });
}

@reflectiveTest
class AvoidPrintRuleTest extends AnalysisRuleTest {
  @override
  void setUp() {
    Registry.ruleRegistry.registerLintRule(AvoidPrintRule());
    super.setUp();
  }

  @override
  String get analysisRule => 'avoid_print_in_code';

  /// print() 使用時に診断が報告されることを確認
  void test_reports_print_usage() async {
    await assertDiagnostics(
      r'''
void main() {
  print('Hello');
}
''',
      [lint(16, 5)],
    );
  }
}

assertDiagnosticsは、指定したコードに対して期待する診断が報告されることを検証します。lint(16, 5)は「オフセット16から長さ5の範囲」に診断があることを意味します。

custom_lintとの比較

Analyzer Pluginsとcustom_lintの機能比較
項目 Analyzer Plugins custom_lint
実行方法 dart analyzeで直接実行 dart run custom_lintが必要
サポート 公式 サードパーティ
デバッグ print不可、ログファイル必須 print可、ブレークポイント可
コード変更の反映 再起動必須 即座に反映

現状では custom_lint の方がデバッグのしやすさやホットリロード対応など開発体験が優れています。一方で、Dart 3.10で公式のAnalyzer Pluginsがリリースされたことで、今後はエコシステム全体が公式機能に統一されていく流れが予想されます。既存プロジェクトを急いで移行する必要はありませんが、新規プロジェクトや長期運用を見据える場合は Analyzer Plugins の採用を検討してみてはいかがでしょうか。

参考リンク