Analyzer Pluginを試してみた

Analyzer Pluginを試してみた

Dart 3.10から公式のAnalyzer Pluginが使えるようになりました。レイヤー間の依存方向をチェックするルールを実装して、その実装方法と使い心地を試してみました。
2026.07.21

はじめに

プロジェクト固有の規約を機械検査したいとき、独自lintといえばcustom_lintを使うことが多かったと思いますが、
Dart 3.10(Flutter 3.38)から 公式のAnalyzer Pluginが使えるようになっています。
analysis serverに直接読み込まれる、公式のプラグイン機構です。

「レイヤー間の依存方向をチェックする」ルールを題材に、実際に書いて動かしてみました。

Analyzer Pluginの良さ

書いてみて感じた良さは、この3つです。

  • 構成がシンプル: analysis serverに直接読み込まれるので、常駐する別プロセスが増えない
  • お手本が大量にある: 組み込みlintと同じAPIなので、SDKのlint実装がそのままサンプル集になる
  • 追従待ちがない: analyzer本体と同じAPIなので、SDKやanalyzerを上げるのにサードパーティの対応を待たなくてよい

一番効いたのは1つ目です。
リポジトリ内にDartパッケージを1つ置くだけで、常駐する別プロセスが増えません。

実装例: レイヤー間の依存方向チェック

前提・環境

検証環境は以下です。

  • Flutter 3.44.1(Dart SDK 3.12.1)
  • analysis_server_plugin 0.3.14
  • analyzer 12.1.0

題材として、lib/直下のディレクトリをレイヤーとみなし、許可されていない方向のimportを警告するルールを書いてみます。

ui → application → domain ← infra

矢印の向きにだけimportできる、というルールです(同一レイヤー内と、ui → domainのような飛び越しは許可)。

セットアップ

プラグインは普通のDartパッケージです。
アプリのリポジトリ内にpackages/app_lints/として置き、pub workspaceのメンバーに加えます。

app/
├── analysis_options.yaml
├── pubspec.yaml
└── packages/
    └── app_lints/
        ├── pubspec.yaml
        ├── lib/
        │   ├── main.dart
        │   └── src/
        │       └── rules/
        │           └── layer_dependency.dart
        └── test/
            └── rules/
                └── layer_dependency_test.dart

packages/app_lints/pubspec.yaml

name: app_lints
publish_to: none
resolution: workspace

dependencies:
  analysis_server_plugin: ^0.3.14
  analyzer: ^12.1.0

dev_dependencies:
  analyzer_testing: ^0.2.5
  test_reflective_loader: ^0.4.0

pubspec.yaml

workspace:
  - packages/app_lints

packages/app_lints/lib/main.dart

エントリーポイントはトップレベル変数plugin
この名前は規約なので変えられません。

final plugin = AppLintsPlugin();

class AppLintsPlugin extends Plugin {
  @override
  String get name => 'app_lints';

  @override
  void register(PluginRegistry registry) {
    // warningルールはデフォルトで有効(analysis optionsで個別に無効化は可能)。
    // registerLintRuleはその逆で、利用側が明示的に有効化しないと動かない。
    // チーム全員に効かせたい規約なので、入れた時点で効く前者を使う。
    registry.registerWarningRule(LayerDependency());
  }
}

analysis_options.yaml

有効化はこれだけ。analyzer:の下ではなく、トップレベルのplugins:セクションに書きます。
なおplugins:はワークスペースルート(この構成ならアプリ側)のanalysis_options.yamlにしか書けません。

plugins:
  app_lints:
    path: packages/app_lints

ルール実装

AnalysisRuleを継承し、警告の内容と、コードのどの部分を見るかを宣言します。
今回はimport文を見たいのでaddImportDirectiveを指定します。

class LayerDependency extends AnalysisRule {
  LayerDependency()
    : super(
        name: 'layer_dependency',
        description: 'レイヤー間の依存方向規約に違反するimportを検出します。',
      );

  static const LintCode code = LintCode(
    'layer_dependency',
    "'{0}' レイヤーから '{1}' レイヤーへのimportは依存方向規約に違反しています。",
    severity: DiagnosticSeverity.WARNING,
  );

  @override
  DiagnosticCode get diagnosticCode => code;

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

判定のロジックは以下です。

const allowedDependencies = <String, Set<String>>{
  'domain': {'domain'},
  'infra': {'domain', 'infra'},
  'application': {'domain', 'application'},
  'ui': {'domain', 'application', 'ui'},
};

class _Visitor extends SimpleAstVisitor<void> {
  _Visitor(this._rule, this._context);

  final LayerDependency _rule;
  final RuleContext _context;

  @override
  void visitImportDirective(ImportDirective node) {
    // 自ファイル側のURIはRuleContextから取る。
    final currentUri = _context.libraryElement?.uri;
    // importedLibrary?.uriはimportの「解決結果」。'../../infra/...'のような
    // 相対importも'package:app/infra/...'に正規化されるため、
    // 文字列マッチの実装にありがちな抜け道ができない。
    final importedUri = node.libraryImport?.importedLibrary?.uri;
    final fromLayer = _layerOf(currentUri);
    final toLayer = _layerOf(importedUri);
    // main.dartやgen/のようなレイヤー外のファイルは_layerOfがnullを返すので、
    // ここで対象外になる(DIの配線をするmain.dartのinfra importは素通し)。
    if (currentUri == null ||
        importedUri == null ||
        fromLayer == null ||
        toLayer == null) {
      return;
    }
    // 同一パッケージ内のみ検査(外部パッケージのlib/構成は関知しない)。
    if (currentUri.pathSegments.first != importedUri.pathSegments.first) {
      return;
    }
    if (allowedDependencies[fromLayer]!.contains(toLayer)) {
      return;
    }
    _rule.reportAtNode(node.uri, arguments: [fromLayer, toLayer]);
  }

  String? _layerOf(Uri? uri) {
    if (uri == null || !uri.isScheme('package')) {
      return null;
    }
    final segments = uri.pathSegments;
    if (segments.length < 2) {
      return null;
    }
    final layer = segments[1];
    return allowedDependencies.containsKey(layer) ? layer : null;
  }
}

相対importでも解決後のURIで判定されるので、文字列マッチの実装にありがちな抜け道ができないのが効いています。

テスト

analyzer_testingAnalysisRuleTestを継承して書きます。

@reflectiveTest
class LayerDependencyTest extends AnalysisRuleTest {
  @override
  void setUp() {
    rule = LayerDependency();
    super.setUp();
    newFile(
      '$testPackageLibPath/infra/user/user_repository_impl.dart',
      'class UserRepositoryImpl {}',
    );
  }

  Future<void> test_uiToInfra_reported() async {
    const uri = "'package:test/infra/user/user_repository_impl.dart'";
    const content =
        '''
import $uri;

final repository = UserRepositoryImpl();
''';
    final path = '$testPackageLibPath/ui/user/user_page.dart';
    newFile(path, content);
    await assertDiagnosticsInFile(path, [
      lint(content.indexOf(uri), uri.length),
    ]);
  }
}

ランナーはtestではなくtest_reflective_loadertest_プレフィックスのメソッドを拾う方式)ですが、実行はdart testでOKです。

なお、標準のassertDiagnosticsは固定パスlib/test.dartを解析します。
今回のように自ファイルのパスで挙動が変わるルールでは使えないので、assertDiagnosticsInFileで任意パスを検査しています。

現時点で気づいた課題

CLIの診断が安定しない(Dart 3.12時点)

これが一番大きい。
dart analyze / flutter analyzeがプラグイン解析の完了を待たずに終了して診断を取りこぼすことがあります(dart-lang/sdk#38407)。

手元では出るときは出ます。

$ dart analyze lib/ui/user/user_page.dart
warning - user_page.dart:2:8 - 'ui' レイヤーから 'infra' レイヤーへのimportは
依存方向規約に違反しています。 - layer_dependency

ただ確実ではないので、CIの砦としては当てになりません。
IDEのリアルタイム警告が主役、CIはすり抜けうる、という割り切りが要ります。

ただし解消は近そうです。
issueには、mainブランチのSDK(3.14.0-edge)なら診断が正しく報告されるという検証報告が上がっています(該当コメント)。
2026年7月時点でissueはまだopen、Dartチームが確認中の段階です。

macOSでanalysis serverが落ちる

検証中、Bad state: The analysis server crashed unexpectedlyでanalysis serverごと落ちました。
よく似た症状がdart-lang/sdk#63846で報告されています。

ROOT CAUSE (update, see comments): the kernel code-signing log shows the killed page belongs to ~/.dartServer/.plugin_manager/<hash>/analyzer_plugin/bin/plugin.aot (the compiled dart_code_linter analyzer plugin), rejected with cs_mtime != mtime — i.e. the file was overwritten while another analysis server had it mmap'd. The plugin AOT cache dir is not scoped by Dart SDK version, so two servers on different SDKs recompile+overwrite the same plugin.aot and mutually invalidate each other's mapping. (...)

プラグインのAOTスナップショットのキャッシュがSDKバージョンで分離されておらず、別のanalysis serverに上書きされるとコード署名が無効になってSIGKILLされる、という見立てです。
ただしこれはユーザーからの報告で、Dartチームによる確認はまだ付いていません(同じくmacOSでのSIGKILLは#63813にも報告あり)。

手元では~/.dartServer/.plugin_manager/を削除して再生成させると復旧しました。

なおプラグインはanalysis server起動時に一度だけ読み込まれます。
プラグイン側を直したらDart: Restart Analysis Serverが必要で、ルールを育てている間はこれの繰り返しです。

おわりに

dart analyzeでの検出は不安定なままですが、IDE上ではルールがきちんと警告として出ます。
このCLIの問題もmainブランチのSDKでは解消しているという報告があるので、そのうち直りそうです。

独自lintの選択肢として、Analyzer Pluginはかなり良さそうです。

この記事をシェアする

関連記事