Analyzer Pluginを試してみた
はじめに
プロジェクト固有の規約を機械検査したいとき、独自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_testingのAnalysisRuleTestを継承して書きます。
@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_loader(test_プレフィックスのメソッドを拾う方式)ですが、実行は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 compileddart_code_linteranalyzer plugin), rejected withcs_mtime != mtime— i.e. the file was overwritten while another analysis server had itmmap'd. The plugin AOT cache dir is not scoped by Dart SDK version, so two servers on different SDKs recompile+overwrite the sameplugin.aotand 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はかなり良さそうです。









