目次
目次
はじめに
この記事は Flutter Advent Calendar 2021(カレンダー 1)の 9 日目の記事です。
こんにちは kawa(@dev_kawa)です。
コーディング規約は、チームでルールを作り誰が見ても理解できるコードを書くことを目的として導入されます。
一方でコーディング規約が守られているかを担保するために、コードレビューのコストが増加してしまうという課題が発生します。
レビューコストの増加かつコーディング規約が守られない場合、目的が達成されずコードレビューだけが辛くなります。
flutter analyze を実行することで、ある程度は解決できますが、こんな Lint があれば便利なのにというケースがあります。
本記事は、そうした状況を改善するために、Flutter(Dart)において静的解析を自作する方法を紹介します。
(このように表示できます)
お気持ちスター(⭐)で喜びます 😻
https://github.com/kawa1214/import-lint
https://pub.dev/packages/import_lint
この記事で伝えたいこと
- Analyzer Plugin で自作の Lint を作る方法
Analyzer 周りのツールについて
Analyzer は Dart の静的解析をします。
Analysis Server は、DartSDK に含まれているツールの 1 つです。
Analysis Server から送られる JSON 形式のデータを、VSCode の Dart Extension を介して受け取り、VSCode にエラーとして表示できます。(dart code)
- Analyzer
https://pub.dev/packages/analyzer - Dart VSCode Extension
https://github.com/Dart-Code/Dart-Code
Analyzer Plugin について
Analyzer Plugin は Analysis Server のプラグインを作成するためのフレームワークです。
こちらを用いて Lint を作ります。
- Analyzer Plugin
https://pub.dev/packages/analyzer_plugin
Analyzer Plugin で自作の Lint を作る方法
Dart のプロジェクトを作る
dart create で dart のプロジェクトを作成します。
dart create hoge_analyzer_plugin
Analyzer Plugin の呼び出しに必要なファイルを配置
Analysis Server は<host_package>/tools/analyzerplugin
に存在するプロジェクトを読み込みます。プロジェクトに必要なファイルを配置します。
tools/analyzer_plugin/pubspec.yaml
name: hoge_analyzer_plugin_loader
description: bootstrap package of hoge_analyzer_plugin
version: 1.0.0
publish_to: none
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
hoge_analyzer_plugin:
path: /xxx/hoge_analyzer_plugin
# hoge_analyzer_plugin ディレクトリまでのパス
# pub.devに公開する際はバージョンを指定する
tools/analyzer_plugin/bin/plugin.dart
import 'dart:isolate';
import 'package:hoge_analyzer_plugin/plugin_starter.dart' as plugin;
void main(List<String> args, SendPort sendPort) {
plugin.start(args, sendPort);
}
lib/plugin_starter.dart
import 'dart:isolate';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer_plugin/starter.dart';
import 'plugin.dart';
void start(Iterable<String> _, SendPort sendPort) {
ServerPluginStarter(HogeAnalyzerPlugin(PhysicalResourceProvider.INSTANCE))
.start(sendPort);
}
プラグインの実装
次にプラグイン本体の実装を進めます。
ServerPlugin
を継承したHogeAnalyzerPlugin
を作成します。
createAnalysisDriver
でドライバーを作成します。ここで、情報を受け取り、分析結果を通知する処理を行います。
lib/plugin.dart
import 'dart:async';
import 'package:analyzer/dart/analysis/context_builder.dart';
import 'package:analyzer/dart/analysis/context_locator.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/src/dart/analysis/driver.dart';
import 'package:analyzer/src/dart/analysis/driver_based_analysis_context.dart';
import 'package:analyzer_plugin/plugin/plugin.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
class HogeAnalyzerPlugin extends ServerPlugin {
HogeAnalyzerPlugin(ResourceProvider provider) : super(provider);
var _filesFromSetPriorityFilesRequest = <String>[];
// プラグインがAnalyzeの対象とするファイルをGlobで記述します
@override
List<String> get fileGlobsToAnalyze => <String>['/lib/**/*.dart'];
// プラグインの名前を記述します
@override
String get name => 'Hoge Analyzer Plugin';
// Server APIのバージョンを記述します
@override
String get version => '1.0.0';
@override
AnalysisDriverGeneric createAnalysisDriver(plugin.ContextRoot contextRoot) {
final rootPath = contextRoot.root;
final locator =
ContextLocator(resourceProvider: resourceProvider).locateRoots(
includedPaths: [rootPath],
excludedPaths: [
...contextRoot.exclude,
],
optionsFile: contextRoot.optionsFile,
);
if (locator.isEmpty) {
final error = StateError('Unexpected empty context');
channel.sendNotification(plugin.PluginErrorParams(
true,
error.message,
error.stackTrace.toString(),
).toNotification());
throw error;
}
final builder = ContextBuilder(
resourceProvider: resourceProvider,
);
final analysisContext = builder.createContext(contextRoot: locator.first);
final context = analysisContext as DriverBasedAnalysisContext;
final dartDriver = context.driver;
runZonedGuarded(
() {
dartDriver.results.listen((analysisResult) {
if (analysisResult is ResolvedUnitResult) {
final errors = _check(
analysisResult: analysisResult,
);
channel.sendNotification(
plugin.AnalysisErrorsParams(
path,
errors,
).toNotification(),
);
} else if (analysisResult is ErrorsResult) {
channel.sendNotification(plugin.PluginErrorParams(
false,
'ErrorResult $analysisResult',
'',
).toNotification());
}
});
},
(Object e, StackTrace stackTrace) {
channel.sendNotification(
plugin.PluginErrorParams(
false,
'Unexpected error: ${e.toString()}',
stackTrace.toString(),
).toNotification(),
);
},
);
return dartDriver;
}
@override
void contentChanged(String path) {
super.driverForPath(path)?.addFile(path);
}
@override
Future<plugin.AnalysisSetPriorityFilesResult> handleAnalysisSetPriorityFiles(
plugin.AnalysisSetPriorityFilesParams parameters,
) async {
_filesFromSetPriorityFilesRequest = parameters.files;
_updatePriorityFiles();
return plugin.AnalysisSetPriorityFilesResult();
}
@override
Future<plugin.AnalysisSetContextRootsResult> handleAnalysisSetContextRoots(
plugin.AnalysisSetContextRootsParams parameters,
) async {
final result = await super.handleAnalysisSetContextRoots(parameters);
return result;
}
void _updatePriorityFiles() {
final filesToFullyResolve = {
..._filesFromSetPriorityFilesRequest,
for (final driver2 in driverMap.values)
...(driver2 as AnalysisDriver).addedFiles,
};
final filesByDriver = <AnalysisDriverGeneric, List<String>>{};
for (final file in filesToFullyResolve) {
final contextRoot = contextRootContaining(file);
if (contextRoot != null) {
final driver = driverMap[contextRoot];
if (driver != null) {
filesByDriver.putIfAbsent(driver, () => <String>[]).add(file);
}
}
}
filesByDriver.forEach((driver, files) {
driver.priorityFiles = files;
});
}
List<plugin.AnalysisError> _check(
ResolvedUnitResult analysisResult,
) {
// TODO: 実装
// The Element Model
// https://github.com/dart-lang/sdk/blob/e995cb5f7cd67d39c1ee4bdbe95c8241db36725f/pkg/analyzer/doc/tutorial/element.md
// ここでエラー情報を返す
return [];
}
}
抽象構文木を用いて具体的な実装をしますが今回は割愛します。
通知するエラー以下のようになります。
final location = plugin.Location(
filePath,
offset,
length,
startLine,
startColumn,
);
final error = plugin.AnalysisError(
plugin.AnalysisErrorSeverity('INFO'),
plugin.AnalysisErrorType.LINT,
location,
message,
code,
correction: 'Try ...',
hasFix: false,
);
AnalysisErrorSeverity: エラーの深刻度を INFO, WARNING, ERROR のレベルで指定できます。
AnalysisErrorType: エラーの種類を指定できます。
CHECKED_MODE_COMPILE_TIME_ERROR,
COMPILE_TIME_ERROR,
HINT,
LINT,
STATIC_TYPE_WARNING,
STATIC_WARNING,
SYNTACTIC_ERROR,
TODO,
message: 文字列型でコードのどこが間違っているかを示します。
code: 文字列型でエラーコードを表示します。
correction: 文字列型で修正する方法を示します。
注意点
flutter analyze
or
dart analyze
analyze コマンドでは、プラグインで作成した情報は表示できないため、別途 CLI の実装が必要です。
おわりに
ざっくりと analyzer plugin について書きました(アドベントカレンダーギリギリで書いてしまう)。