摘要:
本文實(shí)現(xiàn)了一個(gè)自定義的語法檢查插件塑顺,功能是:當(dāng)新寫一個(gè)dart類襟企,如果類名中包含ViewModel歼指,那么必須添加前綴HDW。在vscode中效果如下:
在網(wǎng)上搜索自定義Dart語法檢查
或自定義Dart lint
最終都會(huì)導(dǎo)向 Customizing static analysis 這篇文檔纺且。文檔中介紹了Dart Static analysis的功能和使用方式。
如在if
語句使用了錯(cuò)誤的變量名稱稍浆,提示如下錯(cuò)誤载碌。
void main() {
var count = 0;
if (counts < 10) {
count++;
}
print(count);
}
提示
error ? Undefined name 'counts'. ? lib/main.dart:3:7 ? undefined_identifier
但是文章標(biāo)題中所謂Customizing(自定義)
指的是自定義修改配置檢查規(guī)則猜嘱、設(shè)置工程中文件的檢查匹配范圍、調(diào)整某些規(guī)則的檢查級別(由warning提升到error)等嫁艇。具體方式是朗伶,首先在工程目錄下添加analysis_options.yaml文件:
include: package:pedantic/analysis_options.1.8.0.yaml
analyzer:
exclude: #忽略檢測的文件配置
- lib/client.dart
- lib/server/*.g.dart
- test/_data/**
strong-mode: #設(shè)置某些規(guī)則為嚴(yán)格模式
implicit-casts: false
linter:
rules: #開啟或禁用某些規(guī)則
avoid_shadowing_type_parameters: false
await_only_futures: true
但這種自定義不是我們想要的。我們想要的不僅僅是Dart官網(wǎng)為我們提供的語法檢查步咪,我們需要自己能去分析當(dāng)前代碼的AST(抽象語法樹)论皆,進(jìn)而寫出一些符合自己團(tuán)隊(duì)內(nèi)部語法約定或業(yè)務(wù)約定的自定義規(guī)則。這可行嗎歧斟?可以的纯丸,而且這個(gè)功能就是Dart Static analysis提供,并且仍然可以配置在analysis_options.yaml中静袖。但是不知為何官網(wǎng)沒有提供相關(guān)的文檔描述和教程觉鼻,網(wǎng)上能搜到的文章也很少,并且寫這種自定義規(guī)則在工程創(chuàng)建和調(diào)試上都會(huì)遇到很多坑
點(diǎn)队橙,所以我打算用一個(gè)自定義規(guī)則的完整示例坠陈,將整個(gè)過程呈現(xiàn)出來。
這個(gè)示例的需求是:當(dāng)新寫一個(gè)dart類捐康,如果類名中包含ViewModel仇矾,那么必須添加前綴HDW。(不要糾結(jié)這個(gè)規(guī)則的實(shí)際意義解总,就當(dāng)是業(yè)務(wù)命名的強(qiáng)約束吧??)
//此處需要報(bào)錯(cuò)贮匕,并提示用戶必須添加HDW前綴
class ViewModel {
}
Analyzer plugin簡介
自定義符合自己團(tuán)隊(duì)內(nèi)部語法約定或業(yè)務(wù)約定的規(guī)則,可以通過analyzer plugin實(shí)現(xiàn)花枫。
通過analyzer plugin寫的這些規(guī)則刻盐,其使用方法,檢查效果劳翰,以及在VSCode或AndroidStudio中的表現(xiàn)形式都與Dart Static analysis提供效果完全相同敦锌。
在了解analyzer plugin是什么之前,我們先想一下佳簸,本文第一節(jié)所說的Dart Static analysis是如何工作的乙墙?為什么工程中的analysis_options.yaml文件配置會(huì)生效,并且它是如何生效的生均?
直觀的答案是”Dart SDK提供的功能“听想,打開github的dart-lang項(xiàng)目,相關(guān)的代碼在pkg下的analysis_server疯特、analysis_cli哗魂、analysis_plugin等文件夾中。我們安裝Flutter后執(zhí)行Flutter doctor會(huì)下載對應(yīng)版本的Dart SDK漓雅,在flutter/bin/cache/dart-sdk/bin目錄下我們可以找到dart sdk提供的工具包录别,其中dartanalyzer是提供語法分析和檢查的”服務(wù)“(這里稱之為服務(wù)而不僅僅是工具)朽色。
可以直接在命令行中執(zhí)行它,對某個(gè)具體的文件進(jìn)行語法分析:
dartanalyzer不僅僅只是一個(gè)命令行工具组题,它可以被理解為一個(gè)本地的服務(wù)器應(yīng)用葫男。我們可以指定目錄參數(shù)開啟一個(gè)本地dartanalyzer服務(wù),然后通過跨進(jìn)程的通信通道崔列,將我們需要檢測的文件變成一個(gè)命令發(fā)送給這個(gè)服務(wù)梢褐,服務(wù)會(huì)把檢測好的結(jié)果以約定好的格式返回(具體通信格式可以參考dart-lang下analysis_server)。當(dāng)我們使用VSCode(或AndroidStudio)打開Dart工程時(shí)赵讯,IDE中安裝的Dart插件會(huì)自動(dòng)開啟對應(yīng)于本工程的dartanalyzer服務(wù)盈咳。以VSCode為例,輸入命令>open analyzer diagnostics 可以打開當(dāng)前dartanalyzer服務(wù)對應(yīng)的web信息面板:
這里需要強(qiáng)調(diào)的是边翼,用vscdoe打開多個(gè)Dart項(xiàng)目鱼响,每個(gè)dart項(xiàng)目都會(huì)對應(yīng)不同的服務(wù)(不同的新建的dartanalyzer進(jìn)程實(shí)例)。還有组底,web信息面板默認(rèn)是不啟動(dòng)的丈积,只有顯式的執(zhí)行open analyzer diagnostics才會(huì)開啟。這個(gè)面板中的信息在后面我們自定義檢查規(guī)則中會(huì)用到债鸡。
當(dāng)dartanalyzer啟動(dòng)時(shí)江滨,會(huì)尋找項(xiàng)目目錄下的analysis_options.yaml文件,以此文件中的內(nèi)容作為analyzer的配置信息厌均。比如唬滑,讀取exclude字段,過濾不需要檢查的文件棺弊。
細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn)间雀,在信息菜單中有Plugins選項(xiàng)。沒錯(cuò)镊屎,這里就是今天的主角 analyzer plugin。此時(shí)點(diǎn)擊菜單中Plugins選項(xiàng)茄螃,會(huì)發(fā)現(xiàn)未加載任何Plugins
那什么是analyzer plugin呢缝驳,這里簡介一下:
- 首先一個(gè)analyzer plugin是一個(gè)獨(dú)立的Dart項(xiàng)目,任何人都可以創(chuàng)建自己的plugin工程归苍,建立pubspec.yaml 給工程命名用狱。
- 在analyzer plugin項(xiàng)目中添加一些
約定的東西
就可以被dartanalyzer服務(wù)加載。 - 在analyzer plugin項(xiàng)目中可以獲得dartanalyzer服務(wù)傳入的
編譯單元
進(jìn)而獲得文件代碼的AST拼弃,這樣就可以自己寫代碼進(jìn)行分析處理夏伊。 - 在analyzer plugin項(xiàng)目中可以使用dart analyzer sdk提供的API,將自己分析的結(jié)果吻氧,以
約定的格式
回傳給dartanalyzer服務(wù)溺忧。dartanalyzer服務(wù)根據(jù)回傳內(nèi)容可以提示用戶error 咏连、warning、在IDE(如vscode)中提示用戶如何修改鲁森,提示用戶優(yōu)化建議等等祟滴。
自定義plugin 示例
github上dart-lang中有很詳細(xì)的readme但是其中target package、 host package歌溉、 bootstrap package垄懂、plugin package以及對默認(rèn)加載潛規(guī)則的描述讓人很難一次性成功寫出一個(gè)Demo。這里建議先看我的示例步驟痛垛,運(yùn)行起來后草慧,再回頭閱讀readme,可以事半功倍匙头。
第一步漫谷,建立名為test_plugin的工程
name: test_plugin
version: 0.0.1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
analyzer_plugin: ^0.3.0
quick_log: any
path: any
dev_dependencies:
test: any
第二步,建立啟動(dòng)入口
在當(dāng)前test_plugin工程目錄下建立兩個(gè)文件,(注意這里文件路徑和文件名均是默認(rèn)的潛規(guī)則乾胶,不能自定義改變)
-
./tools/analyzer_plugin/pubspec.yaml
name: test_plugin_bootstrap version: 0.0.1 environment: sdk: '>=2.7.0 <3.0.0' dependencies: test_plugin: path: /Users/david/Desktop/test_plugin # 此處必須是絕對路徑抖剿,下文會(huì)解釋為什么
-
./tools/analyzer_plugin/bin/plugin.dart
import 'dart:isolate'; void main(List<String> args, SendPort sendPort) { print("start"); }
好了,目前我們已經(jīng)有了一個(gè)最最簡單的plugin识窿,下面我們建立一個(gè)test_project斩郎,讓test_project加載這個(gè)plugin。
name: test
version: 0.0.1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
pedantic: ^1.9.2
dev_dependencies:
test_plugin:
path: ../test_plugin/
執(zhí)行pub upgrade并不能加載這個(gè)插件到dartanalyzer服務(wù)中喻频,還需要配置一下analysis_options.yaml
include: package:pedantic/analysis_options.yaml
analyzer:
plugins:
- test_plugin
在analysis_options.yaml的plugins中添加test_plugin后缩宜,一系列的神奇反應(yīng)發(fā)生了:
- 首先每次analysis_options.yaml發(fā)生改變,VScode 都會(huì)通知到dartanalyzer服務(wù)
- dartanalyzer服務(wù)發(fā)現(xiàn)當(dāng)前需要添加一個(gè)名字叫test_plugin的插件
- 去哪里找這個(gè)test_plugin插件呢甥温?沒錯(cuò)锻煌,就是到當(dāng)前工程的pubspec.lock中查看地址
- 找到test_plugin所在目錄后,會(huì)檢查當(dāng)前目錄有沒有
./tools/analyzer_plugin/pubspec.yaml
和./tools/analyzer_plugin/bin/plugin.dart
這兩個(gè)文件(這就是為什么第一步中這兩個(gè)文件的目錄地址和文件名都不能隨意更改的原因)姻蚓。確定文件存在后會(huì)將analyzer_plugin目錄copy到dartServer下的緩存區(qū)中宋梧,(注意是直接復(fù)制過去,之后運(yùn)行的代碼也是copy后的代碼狰挡,這就是為什么后面每次修改plugin.dart中的內(nèi)容要重啟dartanalyzer服務(wù)的原因捂龄,并且也是為什么上面path中必須是絕對路徑的原因)- dartanalyzer服務(wù)此時(shí)會(huì)啟動(dòng)當(dāng)前analyzer_plugin/bin/plugin.dart中的main方法,將插件加載起來
這時(shí)我們重新打開web信息面板:
可以看到加叁,test_plugin已經(jīng)被成功加載到test_project啟動(dòng)的dartanalyzer服務(wù)中了(PS:可以稍稍思考一下這句話三個(gè)單詞對應(yīng)項(xiàng)目的關(guān)系)倦沧。但是最后一行顯示 not running for unknown reason,這是因?yàn)槲覀冊趍ain函數(shù)中啥也沒寫:
void main(List<String> args, SendPort sendPort) {
print("start");
}
這里的sendPort對應(yīng)就是當(dāng)前dartanalyzer服務(wù)它匕,由此開始展融,我們就可以寫具體的邏輯了。我們當(dāng)然可以將代碼都寫在main函數(shù)所在的文件豫柬,但是由于analyzer_plugin下的內(nèi)容是直接copy到緩存中的告希。將所有邏輯代碼寫在test_plugin的lib下是個(gè)更好的選擇扑浸。在lib下添加三個(gè)文件:
start.dart最簡單,就是提供一個(gè)全局方法讓main函數(shù)調(diào)用
具體看下:
starter.dart
void start(List<String> args, SendPort sendPort) {
mirrorLog.info('-----------restarted-------------');
ServerPluginStarter(MirrorPlugin(PhysicalResourceProvider.INSTANCE))
.start(sendPort);
}
其中MirrorPlugin是繼承ServerPlugin自定義的插件類暂雹,通過ServerPluginStarter將其加載首装。具體代碼在mirror_plugin.dart中:
class MirrorPlugin extends ServerPlugin {
@override
void contentChanged(String path) {
// 每次在vscode中修改文件都會(huì)觸發(fā)
mirrorLog.info("contentChanged$path");
AnalysisDriverGeneric driver = super.driverForPath(path);
driver.addFile(path);
}
@override
AnalysisDriverGeneric createAnalysisDriver(plugin.ContextRoot contextRoot) {
// 插件加載時(shí)調(diào)用
final dartDriver = contextBuilder.buildDriver(analysisRoot);
runZonedGuarded(() {
// 創(chuàng)建一個(gè)監(jiān)聽服務(wù),沒有文件改動(dòng)或新建文件杭跪,都會(huì)觸發(fā)listen
dartDriver.results.listen((analysisResult) {
_processResult(dartDriver, analysisResult);
});
}, (e, stackTrace) {
channel.sendNotification(
plugin.PluginErrorParams(false, e.toString(), stackTrace.toString())
.toNotification());
});
return dartDriver;
}
void _processResult(
AnalysisDriver driver, ResolvedUnitResult analysisResult) {
// 具體處理每個(gè)編譯單元的語法分析仙逻,注意這里是Resolved Unit,也就是說我們可以從AST中直接獲取對應(yīng)的element涧尿。(PS:這句注釋理解起來比較費(fèi)力的話系奉,可以先看下官網(wǎng)關(guān)于Dart語法樹相關(guān)的文檔)
try {
if (analysisResult.unit != null &&
analysisResult.libraryElement != null) {
// 將編譯單元丟給自定義的MirrorChecker處理,下面會(huì)分析MirrorChecker
final mirrorChecker = MirrorChecker(analysisResult.unit);
// 獲取分析后的結(jié)果
final issues = mirrorChecker.enumToStringErrors();
mirrorLog.info("MirrorCheckerissues: $issues");
if (issues.isNotEmpty) {
channel.sendNotification(
// 將結(jié)果發(fā)回給dartanalyzer服務(wù)姑廉,vscode會(huì)自動(dòng)顯示在編輯器中
plugin.AnalysisErrorsParams(
analysisResult.path,
issues
.map((issue) => analysisErrorFor(
analysisResult.path, issue, analysisResult.unit))
.toList(),
).toNotification(),
);
} else {
// 返回空結(jié)果
channel.sendNotification(
plugin.AnalysisErrorsParams(analysisResult.path, [])
.toNotification());
}
} else {
// 返回空結(jié)果
channel.sendNotification(
plugin.AnalysisErrorsParams(analysisResult.path, [])
.toNotification());
}
} on Exception catch (e, stackTrace) {
// 返回空結(jié)果
channel.sendNotification(
plugin.PluginErrorParams(false, e.toString(), stackTrace.toString())
.toNotification());
}
}
}
最后也是最關(guān)鍵的文件缺亮,mirror_visitor.dart
class MirrorChecker {
Iterable<MirrorCheckerIssue> enumToStringErrors() {
final visitor = _MirrorVisitor();
visitor.unitPath = unitPath;
_compilationUnit.accept(visitor);
return visitor.issues;
}
}
// 創(chuàng)建一個(gè)集成與RecursiveAstVisitor的語法樹Visitor
class _MirrorVisitor extends RecursiveAstVisitor<void> {
@override
void visitClassDeclaration(ClassDeclaration node) {
// 在本示例中只需要檢查ClassDeclaration語法節(jié)點(diǎn)即可
node.visitChildren(this);
if (node.declaredElement.displayName.contains('ViewModle') &&
!node.declaredElement.displayName.startsWith('HDW')) {、
// 判斷當(dāng)前類是ViewModle 但是沒有添加HDW業(yè)務(wù)前綴時(shí)桥言,添加一個(gè)錯(cuò)誤報(bào)告
_issues.add(
MirrorCheckerIssue(
plugin.AnalysisErrorSeverity.ERROR,
plugin.AnalysisErrorType.LINT,
node.offset,
node.length,
'您的模型類未添加HDW前綴',
'可以改為HDW${node.declaredElement.displayName}',
),
);
}
}
}
好了萌踱,到目前為止,所有的關(guān)鍵代碼都寫好了号阿。我們回到test_project工程, 執(zhí)行>restart analysis server 重啟當(dāng)前工程的dartanalyzer服務(wù)并鸵。
檢查一下信息面板, 成功運(yùn)行結(jié)果如下(有些時(shí)候緩存不會(huì)更新的,自己吧.plugin_manager/xxxxx/ 下面的緩存刪除就行):
在vs中的效果:
是不是很酷??
有沒感覺哪里不對勁扔涧,是的园担,這么多代碼不可能一次性寫出的。我們一般都是寫一點(diǎn)調(diào)試一點(diǎn)枯夜。那么這個(gè)plugin可以調(diào)試運(yùn)行嗎弯汰?很遺憾!不能完整的調(diào)試運(yùn)行湖雹!
但沒關(guān)系咏闪!
如果你要自己開發(fā)plugin的話,首先除了MirrorVisitor以外摔吏,其他部分的代碼都直接使用本示例demo中的代碼即可汤踏。主要自定義的邏輯都在MirrorVisitor中。我寫了一個(gè)測試用例舔腾,點(diǎn)擊即可斷點(diǎn)調(diào)試你自己的MirrorVisitor了
關(guān)鍵代碼寫好后,我們總歸是要在工程中實(shí)際檢驗(yàn)的搂擦,這時(shí)怎么辦稳诚?使用mirrorLog.info("xxxxxx");
這其實(shí)是個(gè)不是辦法的辦法,其實(shí)就是寫入文件
到logger/log.dart的最后一行 把注釋調(diào)整一下瀑踢,(額 打開就知道我再說什么了)
重啟dartanalyzer服務(wù)扳还,這時(shí)桌面上就有了一個(gè)output.log文件
在自己感覺可能出問題代碼附近 使用 mirrorLog.info("xxxxxx");寫入一些日志
在整個(gè)插件運(yùn)行過程中output.log的內(nèi)容會(huì)持續(xù)添加才避,可以在終端執(zhí)行 tail -f ~/Desktop/output.log 實(shí)時(shí)查看日志。
寫好的test_plugin可以發(fā)布到pubsepc上氨距,這樣組內(nèi)就可以共同使用同樣的自定義規(guī)則了桑逝。
文中所有代碼都上傳到這里了,喜歡就給個(gè)贊吧俏让!