kawa.dev

BlogSlide
この記事で伝えたいことAnalyzer周りのツールについてAnalyzer PluginについてAnalyzer Pluginで自作のLintを作る方法Dartのプロジェクトを作るAnalyzer Pluginの呼び出しに必要なファイルを配置プラグインの実装注意点おわりに

Analyzer Pluginで欲しいLintを自作する

2021-12-09

Flutter

/

Dart

/

Lint

この記事はFlutter Advent Calendar 2021(カレンダー1)の9日目の記事です。⛄

こんにちはkawa(@dev_kawa)です。

コーディング規約は、チームでルールを作り誰が見ても理解できるコードを書くことを目的として導入されると思います。

一方でコーディング規約が守られているかを担保するために、コードレビューのコストが増加してしまうという課題が発生します。

コストが増加しコーディング規約が守られないと、目的が達成されずコードレビューだけが辛くなってしまうという悲しいことになってしまいます。

flutter analyzeを実行することで、ある程度は解決できますが、〇〇というLintがあれば便利なのになぁ…というケースがあると思います。

本記事は、そうした状況を改善するために、Flutter(Dart)において静的解析を自作する方法を紹介します。

vscode

(このように表示できます)

お気持ちスター(⭐)で喜びます😻

https://github.com/kawa1214/import-lint

https://pub.dev/packages/import_lint

この記事で伝えたいこと

  • Analyzer Pluginで自作のLintを作る方法

Analyzer周りのツールについて

AnalyzerはDartの静的解析をします。

Analysis Serverは、DartSDKに含まれているツールの一つです。

Analysis Serverから送られるJSON形式のデータを、VSCodeのDart Extensionを介して受け取り、VSCodeにエラーとして表示することができます。(dart code)

Analyzer Pluginについて

Analyzer PluginはAnalysis Serverのプラグインを作成するためのフレームワークです。

こちらを用いてLintを作ります。

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で記述します List<String> get fileGlobsToAnalyze => <String>['/lib/**/*.dart']; // プラグインの名前を記述します String get name => 'Hoge Analyzer Plugin'; // Server APIのバージョンを記述します String get version => '1.0.0'; 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; } void contentChanged(String path) { super.driverForPath(path)?.addFile(path); } Future<plugin.AnalysisSetPriorityFilesResult> handleAnalysisSetPriorityFiles( plugin.AnalysisSetPriorityFilesParams parameters, ) async { _filesFromSetPriorityFilesRequest = parameters.files; _updatePriorityFiles(); return plugin.AnalysisSetPriorityFilesResult(); } 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について書きました。

足りないところが結構あると思うので、(多分)あとで追記します

(アドベントカレンダーギリギリで書いてしまう…)