自定義Flutter Lint插件實(shí)現(xiàn)自己的Dart語法規(guī)則(靜態(tài)代碼檢查分析)

摘要:

本文實(shí)現(xiàn)了一個(gè)自定義的語法檢查插件塑顺,功能是:當(dāng)新寫一個(gè)dart類襟企,如果類名中包含ViewModel歼指,那么必須添加前綴HDW。在vscode中效果如下:

1.png

在網(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ù)而不僅僅是工具)朽色。

2.png

可以直接在命令行中執(zhí)行它,對某個(gè)具體的文件進(jìn)行語法分析:

3.png

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信息面板:

4.png
5.png

這里需要強(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

6.png

那什么是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 packagehost 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
7.png

第二步,建立啟動(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。

8.png
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/


9.png

執(zhí)行pub upgrade并不能加載這個(gè)插件到dartanalyzer服務(wù)中喻频,還需要配置一下analysis_options.yaml

include: package:pedantic/analysis_options.yaml

analyzer:
  plugins:
    - test_plugin
10.png

在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信息面板:

11.png

可以看到加叁,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è)文件:

12.png

start.dart最簡單,就是提供一個(gè)全局方法讓main函數(shù)調(diào)用

13.png

具體看下:

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ù)并鸵。

14.png

檢查一下信息面板, 成功運(yùn)行結(jié)果如下(有些時(shí)候緩存不會(huì)更新的,自己吧.plugin_manager/xxxxx/ 下面的緩存刪除就行):

15.png

在vs中的效果:

16.png

是不是很酷??

有沒感覺哪里不對勁扔涧,是的园担,這么多代碼不可能一次性寫出的。我們一般都是寫一點(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了

17.png

關(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è)贊吧俏让!

參考文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末楞遏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子首昔,更是在濱河造成了極大的恐慌寡喝,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件勒奇,死亡現(xiàn)場離奇詭異预鬓,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)赊颠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門格二,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人竣蹦,你說我怎么就攤上這事顶猜。” “怎么了草添?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵驶兜,是天一觀的道長。 經(jīng)常有香客問我远寸,道長抄淑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任驰后,我火速辦了婚禮肆资,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘灶芝。我一直安慰自己郑原,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布夜涕。 她就那樣靜靜地躺著犯犁,像睡著了一般。 火紅的嫁衣襯著肌膚如雪女器。 梳的紋絲不亂的頭發(fā)上酸役,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼涣澡。 笑死贱呐,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的入桂。 我是一名探鬼主播奄薇,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抗愁!你這毒婦竟也來了馁蒂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤驹愚,失蹤者是張志新(化名)和其女友劉穎远搪,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逢捺,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡谁鳍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了劫瞳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片倘潜。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖志于,靈堂內(nèi)的尸體忽然破棺而出涮因,到底是詐尸還是另有隱情,我是刑警寧澤伺绽,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布养泡,位于F島的核電站,受9級特大地震影響奈应,放射性物質(zhì)發(fā)生泄漏澜掩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一杖挣、第九天 我趴在偏房一處隱蔽的房頂上張望肩榕。 院中可真熱鬧,春花似錦惩妇、人聲如沸株汉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乔妈。三九已至,卻和暖如春氓皱,著一層夾襖步出監(jiān)牢的瞬間褒翰,已是汗流浹背贮懈。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留优训,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓各聘,卻偏偏與公主長得像揣非,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子躲因,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容