ESLint Plugin 入門

ESLint

最近寫了個簡單的 ESLint plugin,來記錄一下一些基礎知識,我做的 plugin 很簡單,叫做eslint-plugin-no-parameter-e,這個 ESLint plugins 做的事情只是檢查所有 function 的參數,然後如果有任何一個參數名是e的話就警報,這條 rule 其實是為了避免把errorevent簡寫成e,會容易混淆。

接下來進入正題,ESLint 基本上就是透過ESPree這個 parser 先把程式碼轉為ESTree相容的 AST,EStree 是個 de facto standard,是從 Mozilla Spider Monkey 用的 AST 演化而來,現在幾乎做 JavaScript 工具,會需要轉 AST 的話都會用這個格式;有了 AST 後,才來分析 AST 做檢查,然後現在有工具叫AST Explorer,非常方便,可以線上直接修改 code sample 看 AST 變化,可以用它來看你想要處理的 code 的 AST 結構,至於怎麼寫 rule 就看個人了,基本上就是監聽要注意的 node,然後檢查 AST 結構,有問題就呼叫 report 這樣。

第二點,npm module 的名稱要用eslint-plugin-開頭,官方說的規則,應該不遵守還是可以抓的到,不過就還是遵守一下免的有意外。

第三點,測試其實 ESLint 有 RuleTester 可以拿來寫測試用:

const rule = require('../rule.js')
const RuleTester = require('eslint').RuleTester

const ruleTester = new RuleTester();

ruleTester.run('no-parameter-e', rule, {
  valid: [
    'function a (event) {}',
  ],
  invalid: [
    {
      code: 'function e (e) {}',
      errors: [{ message }],
    }
  ],
});

很方便,都不用 test framework 了,並且有特別要求 valid 和 invalid 都要有 test case,不然測試就會失敗。

然後測試的時候是每個 rule 獨立跑,每個 plugin 可以有多個 rule,很多 plugin 是把不同 rule 都獨立一個檔案,每個 rule 可以丟的東西除了檢查外還有不少,像是說明文件、自動修復的動作等,詳見官方文件,我一開始是參考eslint-plugin-import的,不過現在初心者應該也可以先看我的eslint-plugin-no-parameter-e,東西更少一些。

下一個想來挑戰處理空行,看了一下感覺是比較困難啊~


Immer 原理

前陣子有個蠻有趣的 library 叫Immer,是 MobX 的開發者 Michel Weststrate 做的,這個 library 做的事情很有趣,它整合了 immutable 資料和原生資料的特性,反過來從缺點來看,immutable 資料型態的問題就是操作比較不方便,所有的修改動作都要透過 method 來執行,不能直接用 assign 的,有時候要改比較深層一點的資料就很麻煩,像 Facebook 的immutable.js就需要用getInupdateIn來處理:

getIn({ x: { y: { z: 123 }}}, ['x', 'y', 'z']) // 123

const original = { x: { y: { z: 123 }}}
setIn(original, ['x', 'y', 'z'], 456) // { x: { y: { z: 456 }}}

用陣列丟每層的屬性名稱,也有一些是用.切分的 path 來處理這個問題(像是prop1.prop2.prop3這種結構),而原生資料的缺點,在這個場景來看當然就是不 immutable 了,Immer 就提出了一個新的構想,把這兩者的優點結合在一起,讓資料可以保持 immutable 特性,又可以直接修改,當然不能直接修改 JavaScript 行為,所以還是有些地方需要等價交換,就是修改資料的時候,要包進 produce function 內:

const nextState = produce(baseState, draftState => {
    draftState.push({ todo: "Tweet about it" })
    draftState[1].done = true
})

然後得到的nextStatebaseState就會是不同物件,就像是 immutable 物件一樣行為,所以如果沒修改就還是同個物件,初看覺得有點黑魔法,不過思考過後覺得也不是不能做,有了些假想的實做方法後去研究了一下程式碼,不太意外的其實在 produce 裡面拿到的 draft 物件,是一個Proxy包裝過的物件,然後 immutable 相關的邏輯都做在 Proxy 內,produce 跑完後再把新的值 finalize 取出用 plain object 傳回給nextState,當然因為 Proxy 是比較新的東西,所以針對 ES5 也有另外的處理,我大致看一下就是比較土法煉鋼的下去比對,至於為什麼不全部都這樣做應該是效能考量吧。其實我覺得比起實做的原理,能想到這樣設計實在是很厲害,不像大部分人早就放棄了,還持續思考是不是有更好的作法可以整合兩種資料格式的優點才有機會找到這條路。

最後,Immer 這名字的由來,雖然在德文有這單字,不過我判斷應該還是從 immersive 來的吧。


2017

神農老街

拖稿很久的 2017 回顧,今年比較沒時間,所以只挑了一輪就直接上陣了(然後還晚了好幾個月),一月是現在固定的台南春節,台南還看的到很多手寫春聯,而且都寫得很漂亮。

閱讀「2017」全文

PEG.js

pegjs

知道這東西也好一陣子了,最近才真的第一次用,感覺還不錯,很久沒有因為東西會動而這麼高興了,大概也是太久沒努力離開舒適圈的關係吧。

總之,最近想著要做出類似一些搜尋引擎支援的條件語法,像是 and、or、not 之類的,稍微花了點時間調查一下確定要正確的處理就是要個 parser,沒錯,就是 compiler 最前面那個 parser,身為非 CS 領域出身的人,compiler 我一直是朦懂朦懂的,parser 到產生 AST 那塊算是比較清楚一些,因為像是 Babel、還有以前幫忙過的TernJS都是先 parse 程式碼產生 AST 才開始做事,不過這次和以前不一樣的是我要從頭開始建立一個語法的 parser,然後因為是網頁前端要用的,所以就找到了PEG.js這個用 JavaScript 寫的 parser generator,相較於手工的 parser,這種工具只要有定義好的語法(grammer)給它,它就可以產生出對應的 parser,至於什麼是語法(grammer)呢,例如下面這段就是:

IdentifierName ::
    IdentifierStart
    IdentifierName IdentifierPart

IdentifierStart ::
    UnicodeIDStart
    $
    _
    \ UnicodeEscapeSequence

IdentifierPart ::
    UnicodeIDContinue
    $
    _
    \ UnicodeEscapeSequence
    <ZWNJ>
    <ZWJ>

UnicodeIDStart ::
    any Unicode code point with the Unicode property &ldquo;ID_Start&rdquo;

UnicodeIDContinue ::
    any Unicode code point with the Unicode property &ldquo;ID_Continue&rdquo;

這段是從 ECMAScript Spec 內找出來的,identifier 名稱格式的語法(grammer)定義,其實還算蠻好理解的,而 PEG.js 也有自己定的語法格式,只要使用該格式定義好語法,就可以產生出 parser 來,不過當我開始寫的時候,才發現到一個問題:我不知道 parse 後要產生什麼東西,這時我才意識到,在開始定義語法之前,我應該要先想清楚後續的產出物(例如 AST)的結構,和要如何使用這個 parser 的產出物實做出真正想要的效果。

以我的目標來說,我希望可以做出簡單的邏輯組合,包括 and、or、not 和 parenthesized expression(括號包起來的),其實我一開始的想法也沒很明確,只是覺得應該可以用樹狀結構加上遞迴來實做後面的判斷,然後參考了Kibana裡面 Kuery 的語法,也算是慢慢的把語法和 AST 的組合方式定義出來,當時做的語法我還有放在gist上,語法和 AST 定義好的時候,其實後面應用端的 script 還沒寫,不過因為結構很簡單,所以我已經確信一定可以運作了,後來隔一天果然不花什麼時間就把應用端的 script 也寫好,之後還花時間作了些手工測試,修正了一些語法上的細節問題,像是支援&|這些符號之類的,還有符號兩邊不用空格等等。

還有一點想特別說的是,其實一開始定義語法的時候,我是沒有想要去參考 Kibana 的,雖然我當時就知道 Kibana 的 Kuery 語法和我的需求很像,而且也是用 PEG.js 做的,不過我開始寫語法定義沒多久就卡關了,卡關的地方就是,一開始就是 and、or、not、parenthesized expression 都有可能出現,但是這無法用/的方式來處理,因為 PEG.js 的 parser 不會解析到一半發現不對就游標往回退(backtracking),然後我就卡關了,我可以寫出 and 加上判斷,支援以下兩種查詢:

keyword
keyword1 and keyword2

但是卻無法更進一步加上支援or,結果只好去參考 Kuery 語法,發現奇妙的寫法,以下是我後來成品的定義:

start
  = orQuery?

orQuery
  = left:andQuery Or right:orQuery
  / andQuery

andQuery
  = left:notQuery And right:andQuery
  / notQuery

notQuery
  = Not right:subQuery
  / subQuery

subQuery
  = '(' ws* query:orQuery ws* ')'
  / queryValue

如此,or查詢支援兩種內容,第一種是and查詢語句,第二種才是真的or查詢,但是他的第一個元素是and查詢,也就是說雖然是or查詢的判斷,但是卻先去看有沒有and查詢,然後and查詢也是類似的定義,實際上先去找有沒有not的語句,然後not會去看有沒有子查詢(parenthesized expression),整個讓人覺得很神奇,仔細下去推敲也確實可以理解判斷的過程,不過在邏輯上我還不太能完全通透的理解。第一次看到這種定義方式時,覺得很神妙,不過也有想說這應該是什麼常見的 grammer 寫法,後來去查了一下 ECMAScript Spec,發現也是這樣的作法,看來真的算是個 convention 了吧(看起來是left recursive),真不知道第一個寫出這種 grammer 的人腦袋裝什麼。

最後我的成果有丟一個可以讓人用的版本上 GitHub,也有用 NPM 發佈,叫simple-search-query,詳細用法可以參考 README,至於完整的語法定義就在query目錄內,還在補測試就是。


HTC U11+

HTC U11+

去年想說要把用了兩三年的 HTC M9 換掉,在 M9 之前其實我 M8、M7 都有用,算是三代機皇的使用者,本來這次想要買 Google Pixel 2 的,不過台灣一直沒上市,直到前幾天 Google 才說要在台灣上市,不過總之,最後考量價錢、機能等等,決定買號稱本來是 Pixel 2 XL 的 HTC U11+,然後也決定要買主打的透明黑,不過可能因為生產問題,透明黑比較晚上市,其實當時差不多是去年的 1111,各家都有特價什麼的,但是卻買不到透明黑感覺實在有點尷尬,連 HTC 自家網站,當時也有些活動,買 U11+ 就可以用很好的折扣家買周邊,有限期間,於是我就每天上去看,看了好幾天覺得,應該沒這麼會做決策吧,結果一如預期的,特價活動結束隔天才有得下單預定透明黑... 我當然是不差買那些周邊啦,不過對於這種搞法實在是無法參透,總之我還是第一天就下定了,後來也比一堆開箱文還早兩三天拿到,不過拖到今天才來記錄一下。

整體而言我覺得這台還蠻不錯的,透明黑玻璃很漂亮,價錢 ok,定價和五吋的高階機種(U11)一樣,對比於 Pixel 的價位真的是親切很多,而且規格和螢幕都比較大,電池電量表現也很不錯,出門一整天也不用擔心沒電,U11 開始主打的 Edge Sense,拿來單手拍照真的很不錯,相機表現也不錯,不過那個側框啟動叫出目錄的功能就不太行了,主要是反應太慢,還不如用指紋直接開畫面,另外一點就是指紋速度很快,還有防塵防水,然後初期宣傳有說全面屏,不過實際上只是正面的玻璃是全部覆蓋,沒有接縫,或許 Google 當初要捨棄實體鍵打的就是這個主意。

不過 U11+ 還是有點問題,其中我比較有感的就是要用 Google Map 導航會進不了導航狀態,這問題在官方論壇上也有討論,暫時的解法是重開機可以修復,所以還可以接受,除此之外如果還要挑,大概就是螢幕還沒到 4K 吧,不過是廣色域的其實也蠻不錯了,聽說今年的 U12 就會是 4K 螢幕了,不過我應該下一台真的會買 Pixel 了吧。


➡ 看看其它文章