用「兩個 VSCode extension + 一個 CLI」取代為了各語言 highlight/lint/format 而 安裝的一大堆零散 extension。編輯器與 CI 共用同一個 binary、同一份設定,所以本機存 檔跟 pipeline 的結果一定一致。
| 產出物 | 職責 |
|---|---|
poly-syntax-highlight |
多語言 syntax highlighting,接管全部 VSCode 內建語言文法 |
poly-lsp |
存檔即時 lint/format、批次命令、編輯器端便利功能 |
poly |
單一 binary CLI,供 CI、pre-commit、終端批次使用 |
兩個 extension 同版號一起發版,但彼此不相依,各自可以單獨裝。
- 156 個文法:接管 49 個 VSCode 內建語言,另加 49 個內建沒有的語言 (HCL/Terraform、nginx、zig、dotenv、protobuf、mermaid、caddyfile、systemd unit、jsonnet、just、nix、cabal、dune、ssh_config、Solidity/Cairo/Vyper、 DBML、PlantUML、CSV/TSV rainbow…)。DBML 另附 snippets 與檔案圖示,PlantUML 另附 snippets。
- 來源共 97 條、31 個 pinned 上游 repo;只有 CSV/TSV/ssh_config 三個是自產的 (上游要嘛不存在,要嘛沒有授權檔)。
- 輸出標準 TextMate scope,任何現有 color theme 直接生效,不自帶配色。
- 部分語言改採比內建更好的社群文法(如 rust 用 dustypomerleau/rust-syntax)。
- 文法一律以 pinned commit 從上游 repo/marketplace VSIX 同步,不手改。
- markdown 清單按 Enter 自動接續下一項(
-/*/+、1./1)、- [ ]、>), VSCode 內建沒有這個行為。同一份規則也套用在SKILL.md、*.prompt.md、*.instructions.md、.claude/agents/**、.claude/rules/**這些 VSCode 1.120 起 不再算markdown的檔案上。有序清單接出來的是1.(poly fmt保留這種寫法)。 - 零執行期程式碼,不佔 extension host 資源。
- Format:Format Document/
editor.formatOnSave,加上批次命令 Format File/Folder/Workspace/Git Repo/Git Changed Files。專案有.editorconfig的話直接沿用,縮排與行寬不必再抄一份到poly.toml。 - Format Selection:只格式化選取的範圍,其餘的行原封不動。
- Lint:存檔即時 diagnostics 進 Problems panel;
Poly: Lint (poly check)在終端跑完整 CLI。 Poly: Minify(cmd+alt+m/ctrl+alt+m):把當前 buffer 壓成一行,涵蓋 JSON/JSONC、CSS、HTML、XML、JavaScript/TypeScript。只移除空白與註解,不改名、 不折常數、不刪分支。刻意不進 format-on-save——它是格式化的反向操作,poly fmt下一次就會把它還原。改動以編輯器 edit 送出而非寫檔,所以 undo 是一個按鍵,未存檔的 buffer 也能用。- 規則說明:滑鼠移到 SQL 的波浪線上會顯示 sqruff 該條規則的 anti-pattern/ best-practice 全文。sqruff 沒有文件站可連,那份說明編在 binary 裡,版本精確、 離線可讀。其他工具有自己的規則頁,走規則代碼上的超連結。
poly.memoryLog(預設關閉):每開一個檔、關一個檔,寫一行 daemon 現在握著什麼: 常駐記憶體、幾份文件與多少位元組、lint 與整包快取、各來源留著幾筆診斷。RSS 只有一個數字,而 poly 有六個地方放東西——這一行的用處是讓漲上去的 數字歸到某一個快取頭上。tools/lsp-smoke.py的 soak 也讀它:120 輪開關之後,poly 握著的每一項都必須跟第 1 輪一樣多(實測把lint_hashes的清除拿掉,RSS 只漂 +0.2 MB 照樣綠,而這條直接指名 hashes 12 → 488)。- 專案內工具優先:偵測到專案的 biome/prettier/eslint/rustfmt 就用它們, 避免和團隊 CI 結果不一致。
- 背景檢查 GitHub Releases(預設 7 天一次,可調可關),一鍵更新裝了的那幾個
extension——沒裝的不會被順手裝上,那不是更新。poly-syntax-highlight 也有自己的檢查與
間隔設定(
poly.syntax.updateCheck.*),只裝它一個也收得到新版。
沒有 CI 對應物的那些,不需要 daemon:binary 沒起來它們照樣能用。附加功能預設全關。
Poly: Copy Path with Line Numbers:複製路徑:行號;選取多行時是路徑:42-51。VSCode 內建的 Copy Relative Path 只到路徑為止,:42是唯一的差別, 但那一截才是重點——src/lib.rs:42正是rg印的、CI annotation 連過去的、終端機 能點的,也是 poly 自己診斷輸出的形狀。預設沒綁快捷鍵,命令面板與編輯器右鍵都有。Poly: Insert Table of Contents:在游標處插入 markdown 目錄,用註釋標記框住, 再跑一次就地更新。錨點照 VSCode 自己的 slug 規則產生,所以連結一定跳得到;front matter 與 code block 裡的#不會被誤認成標題。Poly: Toggle Bold/Toggle Italic:cmd/ctrl+b、cmd/ctrl+i,只在 markdown 檔生效。產生**bold**與_italic_——就是poly fmt正規化出來的那兩種, 不會被下一次存檔改掉。Poly: DBML to SQL/SQL to DBML:DBML 轉成 PostgreSQL/MySQL/SQL Server/ Oracle 的 SQL,或從 PostgreSQL/MySQL/SQL Server/Snowflake/Oracle 的 SQL 反推 DBML。 轉的是編輯器裡的內容,未存檔的修改也算;寫不進去的會指出第幾行第幾欄,存到哪裡用對話框選。 命令面板只在.dbml與 SQL 檔出現。- 簡繁轉換:
Poly: 簡體轉繁體、繁體轉簡體,以及各一個「含台灣用詞」的版本,取代 cipchk.zh-hans-tt-hant-vscode。有選取就轉選取(多重選取每一段都轉),沒有就轉整份。用 OpenCC 的詞組字典:含台灣用詞的那一對會換用語(软件 ↔ 軟體、鼠标 ↔ 滑鼠),另一對只換字;繁體一律是 台灣字形(裡、著)。一次 undo 復原。 - AutoCorrect(
poly.autocorrect.enabled打開):取代 huacnlee.autocorrect,用的是它自己的 引擎。中日韓文字與英數之間補空白、全半形標點、.autocorrectrc裡的詞彙拼寫:開檔與打字時就 標出來,每條一個 quick fix,手動存檔時整份修正(自動存檔與「儲存但不格式化」不會),Poly: AutoCorrect: Format Document手動跑。讀 workspace 資料夾的.autocorrectrc與.autocorrectignore(加上.gitignore),改了立即生效——拿掉一條規則也是。狀態列的 Lint/Format 開關連它一起關;那個 extension 還裝著時 poly 讓開。 - Error Lens(
poly.errorLens.enabled打開):取代 usernamehw.errorlens。每個問題的訊息寫在那一行的 行尾,整行依嚴重度上色;gutter 圖示、狀態列的計數與訊息、CodeLens、hover 裡的按鈕可另外打開。命令面板的Error Lens: …(切換各嚴重度、選取/複製/排除問題、上網搜尋、加上停用此行規則的註解等)關著時也能用。 設定在poly.errorLens.*,名稱同它的。那個 extension 還裝著時 poly 讓開。 - PlantUML:取代 jebbs.plantuml。
alt+d在旁邊預覽游標所在的那張圖(縮放、拖曳、分頁、 複製成圖片),匯出單張/整份檔案/整個 workspace(12 種格式,檔名與out/底下的路徑同 jebbs.plantuml,舊的匯出會被原地覆寫),產生 server URL,從匯出的 PNG 取回原始碼;另有 補全、!define巨集的參數提示、大綱,以及同名圖(匯出會互相覆蓋)與未命名圖的檢查。 jar 由 poly 下載,settings 的poly.tools寫"plantuml": "<jar 路徑>"可改指向別的 jar;Java 自備,不在 PATH 上就設poly.plantuml.java。MIT 版的 jar 畫不了 ditaa,需要就指向 GPL 版的 jar。 jebbs.plantuml 還裝著時 poly 讓開。 - Excalidraw:取代 pomdtr.excalidraw-editor。
.excalidraw、.excalidraw.json、.excalidraw.svg、.excalidraw.png用 Excalidraw 開,存出來的檔案與它的相同:SVG/PNG 存的是 圖片本身並嵌著 scene,放進文件直接能看、再開還能編輯。標題列可切換原始檔、圖片與編輯器, 元件庫可存在工作區裡跟著專案走,主題與語言可設。設定在poly.excalidraw.*,名稱同它的。 介面與手寫字型打包在 extension 裡,中文字型要用時才從 esm.sh 下載。兩個都裝著時 VSCode 會請你 選預設的編輯器。 - Markdown 匯出:取代 yzane.markdown-pdf。
Export Markdown (pdf)/(html)/(png)/(jpeg), 右鍵選單也有,也可以存檔時自動轉換;匯出的檔案與它的相同:語法上色、KaTeX 數學式、PlantUML 與 mermaid 圖、引入別的 markdown 檔,PDF 的紙張、邊界與頁首頁尾可設。設定在poly.markdownPdf.*, 名稱同它的。PDF 與圖片由已安裝的 Chrome 或 Edge 印出;都沒有時第一次匯出才下載固定版本的 Chrome for Testing(macOS arm64 與 Windows x64),核對雜湊值後才使用。emoji 畫成字元,不是它的 Apple 圖片。兩個都裝著時 poly 的命令不放進右鍵選單。 - CodeSnap:取代 adpyke.codesnap。選取程式碼後執行
CodeSnap 📸(右鍵選單也有),旁邊開出的 頁面照 VSCode 的顏色與字型把它畫成 macOS 風格的視窗,按快門存成 PNG 或複製到剪貼簿;拍出的圖與 它的相同。背景、陰影、視窗樣式與行號可設,設定在poly.codeSnap.*,名稱同它的。頁面開著時,選取 會蓋掉剪貼簿,和它一樣。兩個都裝著時 poly 的命令不放進右鍵選單。 - 貼上圖片:取代 mushan.vscode-paste-image。截圖或複製圖片後,在檔案裡按
cmd/ctrl+alt+shift+i(或Paste Image),圖片存成 PNG 放在檔案旁邊,游標處插入連結:markdown 是,AsciiDoc 是image::路徑[]。有選取文字就拿它當檔名,否則以貼上的時間命名。存放的資料夾、連結的寫法與檔名都可設, 設定在poly.pasteImage.*,名稱同它的。它的cmd/ctrl+alt+v在 poly 是 Extract Variable,所以換了鍵; 兩個都裝著時各用各的鍵。 - 資料預覽:取代 RandomFractalsInc.vscode-data-preview。JSON、JSON Lines、JSON5、HJSON、YAML、CSV/TSV、
Markdown 表格、properties/ini/env、Excel 與 ODS、Arrow、Avro、Parquet 檔按編輯器標題的按鈕(或
Preview Data,檔案總管與分頁右鍵也有),開出可排序、篩選、分組的表格,也能換成圖表;網址上的檔也能 預覽。篩選後的資料可另存成別的格式,表格的設定可存成.config再載入。頁面與表格元件(Perspective 0.4)同它的,但不從網路載入任何東西。設定在poly.dataPreview.*,名稱同它的。不綁快捷鍵;兩個都裝著時 poly 的按鈕與選單讓出來。 - Swagger 預覽:取代 arjun.swagger-viewer。在 Swagger 2.0 或 OpenAPI 3 的 JSON/YAML 檔按
shift+alt+p(或Preview Swagger,檔案總管右鍵也有),旁邊開出 Swagger UI,打字時跟著更新;也能 從網址預覽,檔案總管有工作區裡 spec 的清單。頁面與 Swagger UI 的版本同它的。JSON spec 照 schema 標出錯誤,YAML 的要另裝 redhat.vscode-yaml。spec 引用別的檔(外部$ref)時,被引用的檔會併進預覽、 改了也跟著更新;它宣稱支援這點,實際上沒有作用。設定在poly.swaggerViewer.*,名稱同它的。兩個都 裝著時 poly 讓出快捷鍵、右鍵選單與清單。 - Marp 投影片:取代 marp-team.marp-vscode。front matter 寫了
marp: true的 Markdown,預覽就是 Marp 投影片(主題、分頁、數學式、背景圖);directive 有上色、說明、補全與檢查,多半附快速修正。Export Marp Slide Deck...匯出 HTML、PDF、PPTX、PNG、JPEG 或講者備忘稿,Copilot Chat 裡是#polyExportMarp。編輯器標題列的 Marp 按鈕開出命令清單,File > New File有 Marp Markdown。設定在poly.marp.*,名稱同它的(已棄用的enableHtml、chromePath除外)。PDF、PPTX 與圖片用已安裝的 Chrome、Edge 或 Firefox 匯出,和它一樣不另外下載。兩個都裝著時 poly 整個讓出。 - Git History:取代 mhutchie.git-graph。按 status bar 的 Git History、原始檔控制標題列的按鈕(或
View Git History),所有分支、tag 與 stash 畫成一張圖,未提交的變更在最上面。點 commit 在下面展開它的 訊息與變更的檔案,cmd/ctrl加點另一個是比較兩者;可搜尋、只看某些分支。commit、分支、tag、stash 與 檔案的右鍵選單做簽出、merge、rebase、cherry-pick、reset、push、刪除等操作,remote 在工具列管理。功能 照它的行為做;圖的畫法移植自 VSCode 的原始檔控制圖(Source Control Graph,MIT),顏色是它的scmGraph.*佈景主題色。沒有它的git-graph.*設定。兩個都裝著時 poly 讓出 status bar 與原始檔控制的按鈕。 電腦上沒有 git 時改由 poly 讀 repository,圖、commit 細節、比較與 diff 照常,但只能看:會改動 repository 的操作都需要 git。 - Code Runner(
poly.codeRunner.enabled打開):取代 formulahendry.code-runner。ctrl+alt+n(或Run Code,編輯器標題的 ▶ 按鈕與右鍵選單也有)執行目前的檔案或選取的程式碼,結果在輸出面板的 Code Runner,runInTerminal改在終端機;ctrl+alt+k執行自訂命令,ctrl+alt+j先挑語言再執行,ctrl+alt+m停止。 各語言用什麼命令執行照它的表,設定在poly.codeRunner.*,名稱同它的;Go、Rust、Python 的預設改成go run .、cargo run與選定的 Python 直譯器(沒有時python3)。formulahendry.code-runner 還裝著時 poly 讓開。 - 清單接續:在清單項目上按 Enter 接出下一項,有序清單號碼遞增(整份寫成
1.的 清單維持1.),任務項接出- [ ],空的項目按 Enter 結束清單(往外退一層,最外層 就清掉 marker)。markdown 家族與 yaml 都有,yaml 只認 sequence 的破折號——>在那裡是 folded block scalar。 - 清單縮排:游標在清單項目的內容起點或更左邊時,
tab進一層、shift+tab退一層。 一層是上一項內容開始的那一欄——- x的內容在第 2 欄、1. x在第 3 欄,也就是poly fmt正規化出來的縮排,不是editor.tabSize。游標已經在文字裡、有選取、補全清單 開著、Copilot 的 inline suggestion 等著被接受時,Tab 原樣還給編輯器。 - Postfix completion:
err.if展開成if err != nil { }(Go)、if (err) { }(TS)、if err:(Python)。go/rust/swift/ts/js/python/lua/c/cpp 都有。這是 文字重排不是分析——poly 只讀.左邊那串字元塞進模板,不知道err是什麼型別,也正 因為如此同一份表才蓋得住每個語言。排在 language server 的答案後面。 Poly: Extract Variable/Inline Variable:cmd/ctrl+alt+v、cmd/ctrl+alt+shift+v,每個語言都通用。內建的editor.action.refactor開的是一張選單, 而你要的那一項每個 server 講法都不同(Extract variable/Extract into variable/Extract subexpression to variable/Extract to constant in enclosing scope),快捷鍵 綁不到任何一個。poly 問的是 LSP 標準的refactor.extract/refactor.inlinekind, 過濾掉Extract function那種不是變數的,剛好一項就直接套用。做事的是語言自己的 server。 你裝的別的擴充綁了同一組鍵(mushan.vscode-paste-image 的貼圖、quicktype 的 Paste JSON as Types)時 poly 讓出來,命令面板照樣叫得到。Poly: Move to New File/Change Signature/Implement Interface:同一個形狀再 三個,從命令面板叫。Move to New File 問refactor.extract挑toNewFile;Change Signature 在游標原位問refactor.rewrite,游標要在參數上;Implement Interface 問quickfix挑 「補上缺的方法」,要先有一個編不過的斷言(Go 是var _ Shape = Triangle{}),因為 server 是對著診斷提供那條修正的。- 引用與實作 CodeLens:每個宣告一行
11 refs;interface 多一顆3 impls,具體型別有滿足 interface 才多一顆1 interface,方法寫在型別外面的語言(Go)再多一顆4 methods。 只有一筆就直接跳過去,多筆開檔案總管裡的 References 面板——那是 poly 自己的樹,每一列除了 原始碼還帶行號與 它落在哪個符號裡(method Handle、func main),內建的references-view兩欄都沒有, 而別人的樹加不了欄位。數字來自該語言已註冊的 provider,poly 只數與畫。GraphQL 與 nginx 沒有 server 答得出引用,由 poly 自己按名字算:GraphQL 的 type、fragment、directive;nginx 的upstream、location @name、set/map等宣告的變數、limit_req_zone之類的 zone、log_format。範圍是整個 workspace folder,沒有 scope;同一份答案也給 outline 與 Find All References。數字存在$XDG_CACHE_HOME/poly/refs/(預設~/.cache/poly/refs/),重開視窗時先畫上次的 數字、背景重新問過再更新。poly.referencesCodeLens.enabled可關。 run | debugCodeLens:程式進入點(Go/Rust/C/C++/Java 的main、C# 的Main、 Python 的if __name__ == "__main__"、shell 的 shebang)上方一行。run存檔後像 Code Runner 的Run Code那樣執行整個檔案,命令來自poly.codeRunner.*(go run ./cargo run/選定的 Python 直譯器/shebang 指定的直譯器),結果在輸出面板的 Code Runner,不經過 debugger;那些設定 有這個檔的命令就有run,Code Runner 關著也一樣。poly 沒有 debugger,debug是交給你已經裝的 debug extension。poly.runCodeLens.enabled可關。- protobuf → 生成的 Go:
.proto的message/enum上方go type,service上方go server/go client,跳到 protoc 生出來的宣告;rpc上方N impls,跳到寫在 Go 裡的 handler。認 protoc-gen-go 與 protoc-gen-go-grpc 的命名規則;生成檔不在 workspace 裡就不畫。poly.protobufCodeLens.enabled可關。 - 跨檔案 next/previous change +
Poly: Revert Selected Changes and Save:cmd/ctrl+alt+z/cmd/ctrl+alt+a跳到上/下一個有改動的檔案並落在改動上,alt+q還原游標所在的 hunk 並存檔。VSCode 內建的是「同一個檔案裡的下一處改動」,跨檔案那 一步沒有——而那是 review 一個 branch 時按最多次的一步。順序照路徑排,所以同一顆 按鍵按兩次一定走同一條路。別的擴充綁了同一組鍵(Rewrap 的alt+q)時 poly 讓出來。 - 縮排上色:每層縮排的空白塗底色,四色循環;填不滿一層的空白另外標色,那正是 「縮排改到一半」的樣子。內建的 indent guides 畫線回答「block 從哪開始」,上色回答的 是「我在第幾層」。只畫可見範圍,顏色走 theme color。
- Unicode 高亮:gremlins 的替代。不可見字元、雙向控制字元、怪空白、冒充 ASCII 的字元
(en dash、彎引號),所有檔案、邊打邊標:gutter 記號(error 😡、warning 🤔、info ℹ️)、
捲軸刻度、行尾寫出字元名稱,等級與顏色照 gremlins。
poly.unicodeHighlight.enabled打開。 - Gutter 圖片預覽:某行提到的圖檔存在就在 gutter 放縮圖。不寫語法解析器—— markdown/HTML/CSS 各有寫法,而檔案存不存在才是真正的過濾器。
- markdown preview 的 mermaid 圖表:```mermaid fence 在 preview 裡畫成圖,配色與字型
跟著編輯器主題。VSCode 1.135 起內建就有這個功能,那時候 poly 會自動讓開——所以這一項
實際生效的是 1.85 到 1.134。
poly.markdownMermaid.enabled可關。 - markdown preview 的其他圖表:fence 語言為
nomnoml、flowchart(或flow)、sequence、vega、vega-lite、markmap、excalidraw(scene 的 JSON),範圍與畫法同 MarkNote。函式庫在文件第一次用到時才載入; 深色主題下,本身不吃配色的圖畫在淺色底卡上。plantuml(或puml、uml)也在內:有設poly.plantuml.server就交給 server,否則在本機用 Java 畫。poly.markdownDiagrams.enabled打開。 - GitHub 樣式的 preview:取代 Markdown Preview Github Styling,九種 GitHub 配色(含
高對比與色盲友善)、跟隨編輯器或系統的深淺色。
poly.markdownGithubStyle.enabled打開; 那個 extension 還裝著的時候 poly 讓開。 - TODOs 檢視:檔案總管多一個面板,列出整個 workspace 的
TODO/FIXME/HACK/XXX/BUG。只在面板顯示時才掃描,排除規則沿用files.exclude/search.exclude, 而且掃描上限會寫在標題上——「清單很短」跟「清單被截斷」不該長得一樣。 - 語法顏色設定
poly.syntaxColors:使用者 settings.json 的poly區塊裡一張表,項目是 TextMate scope、值是顏色加選用樣式,例如comment→#6A9955 italic。改完畫面當場重新上色, 刪掉那一項就回到 theme 的顏色。一個 scope 也套到它底下更長的 scope,comment涵蓋每一種註解。 主題只讀editor.tokenColorCustomizations,所以 poly 把這張表複製進去,成為名為poly.syntaxColors的 rule;你自己在那裡寫的 rule 與各 theme 專屬的設定不會被動到。 Go、TypeScript、Python 這類有 semantic tokens 的語言,language server 的顏色會蓋在上面, 那部分要改editor.semanticTokenColorCustomizations。 Poly: Set Syntax Color:不知道 scope 叫什麼就用這個。從目前這個檔的文法的 scope 清單挑一個 (可打字過濾,已設定的會顯示目前的值),輸入#C586C0或#C586C0 italic,寫進poly.syntaxColors;留空就刪掉那一項。遠端視窗(WSL、SSH)讀不到文法,改成直接輸入 scope 名稱。Poly: Syntax Colors for This Language:同一份清單整份列出,做成可以直接複製的poly.syntaxColors片段。改配色這件事 VSCode 一直都做得到,卡住的是沒人知道 scope 叫什麼—— 內建的Inspect Editor Tokens and Scopes一次只給游標下的那一個。顏色欄位是#RRGGBB佔位字串而不是某個預設色:整份貼上去不會改變任何顏色,你只會改你改過的那幾條。
- 內嵌引擎(免安裝、離線可用):TypeScript/JavaScript(lint 是 deno_lint 的
recommended 規則集,code 長
deno_lint/*;專案自己裝了 eslint 或 biome 就換它們, 同一份檔案不會被兩套規則各報一次)、JSON/JSONC、 Markdown(格式化,lint 是 rumdl 的 7 條規則,code 長rumdl/MD*——見下面的說明)、 TOML、YAML、CSS/SCSS/LESS、HTML/Vue/Svelte/Astro/Jinja、 Python/Jupyter(格式化與 lint 都是 ruff)、SQL、XML、 GraphQL(格式化,lint 只有語法檢查——見下面的說明)、 Dockerfile(格式化,lint 是 poly 自己寫的規則,code 長poly/docker-*; hadolint 預設關閉,因為它跟 poly 的規則大部分重疊——見下面的外部工具), Lua(格式化 stylua、lint selene)、 PHP(格式化與 lint 都是 mago,lint 是它 190 條規則裡的 7 條——見下面的說明)、 Protobuf(lint 是 poly 自己寫的規則,code 長poly/proto-*;格式化仍是 buf)。 這些都是編進 binary 的 Rust library, 不再下載。拼字檢查(typos)也在裡面,而且不分語言——它讀的是每一個檔案, 包含 poly 認不出語言的那些。 - 外部工具(受管下載):shellcheck、shfmt、actionlint、
tflint、gofumpt、golangci-lint、swiftlint、buf
(Protobuf 的格式化)、
arity(R 的格式化與 lint)、
jsonnetfmt(Jsonnet 的格式化,
.jsonnet/.libsonnet,用它的預設值,不另設旗標)、 PlantUML(MIT 版的 jar,給編輯器的預覽與匯出用,不做 lint;Java 要自己裝)。PATH 上 已經有同一個工具就直接用它,版本不同沒關係,只要主版號跟 poly 釘的一樣(golangci-lint 要 2.x,1.x 的命令列不相容);PlantUML 例外,一律用 poly 下載的 jar。PATH 上沒有才 下載 poly 釘死的版本,每個平台的 sha256 都預先寫進poly-tools.lock並編進 poly—— 下載對不上就直接失敗,而不是信任第一次抓到的東西。 - zsh 只格式化,不 lint(
.zsh)。shfmt 讀得懂 zsh 文法,shellcheck 讀不懂——它只支 援 sh/bash/dash/ksh。以前 poly 把.zsh一起丟給 shellcheck,結果是拿 bash 文法解析 zsh:361 個真實.zsh檔案上產生 2,454 條 findings,佔量測語料裡全部 shellcheck findings 的 55%,其中最多的一條還是叫你替 zsh 根本不會做 word splitting 的展開加引號。現在.zsh是自己的語言,格式化照舊,lint 沒有——覆蓋回報的shellcheck N files也不再把它們算進 去。[format.zsh]是它的設定區段。 - 預設關閉但仍可用:hadolint。poly 現在有自己的 Dockerfile 規則,兩邊一起跑
等於同一個缺陷印兩次、掛兩個 code、兩種嚴重度,
[lint] fail-on會變成看誰先講話。 拿 256 個真實 Dockerfile 量過:hadolint 的 shellcheck findings 是 poly 自己那套 的子集(65 對 534,沒有一個 code 是 hadolint 有而 poly 沒有的),而且位置更差 (每個 RUN 的第 1 欄,poly 指到出問題的那個字)。poly 沒有補的是它三條 info 級 規則:DL3047、DL3059、DL3066。想同時看兩邊就寫[tools] hadolint = "on", poly 會照樣下載並執行它,不會囉嗦。 - actionlint 有兩組檢查關掉了,都是同一個道理:poly 自己已經在做那件事。一是它
的 shellcheck pass——workflow 的
run:由 poly 自己跑 shellcheck,掛shellcheck/SC…並指到出問題的那個字;開著等於同一個缺陷被 actionlint 用 error 再報一次、指在run:那一鍵。二是跟 poly 規則重疊的五個檢查:runner label、step 的未知 key、既沒uses:也沒run:的 step、event filter、permission scope。 - 其中 runner label 那條原本會讓自架 runner 的專案一跑就整片紅。 1,190 個真實
workflow 上,actionlint 全部 926 條 findings 有 655 條是它(70.7%),而其中 621
條是自架 runner 的名字:
amd-medium、blacksmith-4vcpu-ubuntu-2404,某個 repo 自己的 pool 就佔 530 次。它沒有辦法知道那些名字是真的,而且每一條都是 error。poly 的規則只在三種情況出聲——已退役的 image、跟真名差一兩個字、不存在的版本號——其餘 一律當作自架或第三方 runner 放過。另外四個檢查關掉零成本:重複的 69 條全部落在同 一行同一欄,而且 poly 報得不比 actionlint 少(27:14、13:13、7:4、5:4)。代價只有 一項:你如果在.github/actionlint.yaml列了自己的 label,actionlint 讀得到而 poly 讀不到,那份清單裡的拼錯就沒人抓。actionlint 其餘的檢查一條都沒少,它的 expression type checker 更是它留在這裡的全部理由。 - 只用專案 toolchain、不代裝:rustfmt、clang-format、swift-format、
terraform fmt、
cargo clippy。 - Rust 的 lint 是
cargo clippy,範圍是整個 cargo workspace——跟 Go 的 golangci-lint(整個 module)、Terraform 的 tflint(一個目錄)同一個機制:存檔時 在編輯器裡跑的,跟poly check在 CI 裡跑的,是同一次呼叫。build 目錄是target/poly而不是預設的target/,這樣你在終端打的cargo test不會等 編輯器(rust-analyzer 也是這麼做的);代價是多一棵 build tree,第一次會編一次。 不想要就[tools] cargo = "off"。 - TypeScript 裡的
css/html/sql標籤模板會一起格式化,用的是 poly 格式化.css/.html/.sql檔的同一個引擎,所以 styled-components 的樣式、lit 的模板存檔後 跟獨立檔案長得一樣。styled.div與styled(Button)開頭的模板也算 CSS。插值 (${…})原地保留,格式化不會動到它裡面的運算式。標籤是其他名字,或片段本身解析不了 (標籤模板常常只是一個片段,不是完整的檔案),就原樣留著——不會讓整個檔案格式化失敗。 - Protobuf 的 lint 是 poly 自己的規則,不需要 buf module:
.proto上方沒有buf.yaml也照樣檢查(以前這種檔案是整個跳過的)。有buf.yaml的話,它的lint區段——use、except、ignore、ignore_only,v1 v2 都讀——決定哪幾條規則跑,// buf:lint:ignore註釋也照樣有效。poly 這 14 條對應 bufBASIC那層的 單檔規則;bufSTANDARD多加的那批命名慣例(ENUM_VALUE_PREFIX、PACKAGE_VERSION_SUFFIX、SERVICE_SUFFIX等)、需要整個 module 的規則 (PACKAGE_SAME_*、RPC_REQUEST_RESPONSE_UNIQUE…)與需要解析 import 的規則 (IMPORT_USED、PROTOVALIDATE…)都沒有。編譯錯誤現在由poly fmt抓(那仍是 buf)。 poly 的 parser 還不支援edition = "2023",遇到讀不了的檔案會報poly/proto-unreadable——那是「poly 沒檢查這個檔案」,不是「這個檔案有問題」。 格式化不受影響,.proto一律格式化。 - 看起來不是本人的字元(code 長
poly/unicode-*,類別confusable-character, 等級一律 warning)。不分語言,每個檔案都檢查——這是取代 gremlins 那類編輯器裝飾的部分, 差別在於它同樣會在 CLI 與 CI 裡紅。五條規則:-bidi(雙向控制字元,也就是 Trojan Source)、-invisible(零寬字元、軟連字號、ETX/VT 控制字元、行與段落分隔符號 U+2028/U+2029、物件取代字元 U+FFFC;檔首的 BOM 不算)、-space(不斷行空格、全形空格這類「看起來是空白但不是」)、-lookalike(EN DASH 之於-、 彎引號之於'/")、-mixed-script(同一個字裡混了西里爾或希臘字母)。 界線是**「會被誤認成某個 ASCII 字元」而不是「非 ASCII」**,而且是量出來的: 這個 repo 有 1504 個 EM DASH、1124 個全形逗號冒號分號,全都是正確的中文排版, 一併報就是 1162 筆噪音;只報真正會混淆的則是 22 筆。所以 em dash、全形標點、 法文引號都不在規則裡。編輯器要把看不見的字元畫出來,VSCode 內建editor.unicodeHighlight.invisibleCharacters/.ambiguousCharacters就是那個功能, poly 不再畫第二次。 - GraphQL 的 lint 只有語法檢查(code 是
graphql/syntax,等級 error),與 TOML、 TypeScript 一樣是「這個檔案不是它副檔名說的那個語言」。用的是格式化時的同一支 parser (apollo-parser,October 2021 版規格),所以編輯器與 CI 指的是同一個字元。 不做 schema 驗證,這是量過的決定:854 個真實.graphql上,驗證器報的東西有 98.8% 是「定義在別的檔案」——federation 的@link、隔壁模組宣告的 type、只有幾個 type 而沒有 root 的 schema 片段。一份 schema 是好幾個檔案組起來的,而 poly 一次看一個檔案,那些不是 這個檔案的缺陷。 - YAML 與 TOML 照它們指定的 JSON Schema 驗證(code 長
schema/<keyword>,例如schema/required、schema/type、schema/additionalProperties,等級 warning)。schema 由 檔案自己指定,寫法跟 redhat.vscode-yaml 與 even-better-toml 讀的那行相同——YAML 寫# yaml-language-server: $schema=<網址或路徑>,TOML 寫#:schema <網址或路徑>——或在poly.toml的[lint.schemas]用 glob 對應。兩者都沒有就不檢查,poly check的 coverage 只有在真的有檔案被檢查時才多一列schema。錯誤標在出錯的那個 key 或值上:拼錯的 key、 型別不對的值、少了必填欄位的那個區段的 key。網址的 schema 一天抓一次、存在 poly 的快取, 沒網路時用快取的那份;完全讀不到時算「沒檢查到」(結束碼 2),不會當成通過。 SchemaStore 的檔名對應不用,這是量過的決定:1,329 個真實 YAML/TOML 上它對到 569 個, 其中 106 個是錯的檔(一個**/scenarios/*/*.yaml就把 104 個 QA fixture 當成資安工具的設 定檔);而對得對的那些,報出來的東西大多是 schema 比讀它的程式還嚴或還舊。XML 的 XSD/DTD 不做;JSON 由 VSCode 內建的 JSON 支援驗證。 - Markdown 的 lint 是 rumdl 的 7 條規則,全部只報壞掉的東西:相對連結指向不存在的檔
(MD057)、錨點不存在(MD051)、連結寫反
(文字)[網址](MD011)、空連結(MD042)、 參考式連結沒有定義(MD052)、標題跳級(MD001)、圖片沒有 alt(MD045)。code 長rumdl/MD*,rumdl 自己的rumdl-disable/rumdl-disable-next-line註解(<!-- ... -->那種形式,寫成 HTML 註解)照樣有效,要整個關掉某一條就寫[lint] ignore。 版面的規則(行長、標題與清單前後的空行、強調的寫法…)一條都不開,專案自己的.rumdl.toml也不讀:那些是poly fmt的事,開下去等於報poly fmt前一秒才寫出來的 東西——實測 4,947 個檔案,光 MD036 就有 1,054 條是格式化自己造出來的。 一個已知落差:MD051 只答得出同一個檔案裡的錨點。other.md#section這種跨檔錨點, rumdl 自己是先索引整棵樹才能檢查的,而 poly 一次只看一個檔案(編輯器裡本來也只有那一個 檔案),所以不報——同一批檔案裡有 58 條。 - PHP 的格式化是 mago,預設就是 PSR-12 的 120 欄/4 空白,
[format.php]三個 knob 全部有效。.php/.php4/.php5/.phtml/.ctp都認,模板裡的 HTML 不動。 lint 只開 7 條(code 長mago/*,等級 warning):preg_quote()沒給 delimiter、 用==/===比對 token 或密碼、迴圈第一圈就必定跳出、printf佔位符比引數多、finally裡有 return/break、短開頭標籤<?、explode()兩個引數寫反。 另外php/syntax(等級 error)是「PHP 不會跑這個檔案」,與 TOML、TypeScript、GraphQL 同一種說法。其餘 106 條預設規則一條都不開,這是量過的決定:8 個真實 PHP 專案的 20,200 個檔案上,mago 的預設規則集報 83,756 條、命中 81.5% 的檔案,其中三分之二是 「每個檔案都要寫declare(strict_types=1)」「不要用isset」「不要用else」這類 house style;它的 error 等級也有 64% 是複雜度指標而不是缺陷。專案自己的mago.toml不讀,@mago-ignore註釋也不是 poly 的抑制方式——要關某一條就寫[lint] ignore, 單行就寫// poly: ignore mago/<rule>。 - R(
.R/.r)的格式化、lint 與語言功能都是 arity,一支 binary,poly 代抓, 不必先裝 R。專案自己的arity.toml或air.toml(版面、select/ignore)照樣生效,# arity-ignore <rule>: 理由註釋也照樣有效。code 長arity/*;lint 以 R 套件為 單位跑——R/底下互相引用的符號不會被誤報成未定義,編輯器與poly check是同一個答案。 自己宣告是產生出來的檔案不會被重排:開頭八行有註釋寫do not edit的,poly fmt一律跳過——Rcpp 的RcppExports.R、cpp11 的cpp11.R、rlang 的import-standalone-*.R都是這一類。剩下一個已知落差:arity 另外還跳過revdep/與renv/這兩個目錄,poly 不跳,因為那底下是手寫腳本而不是產生出來的(七個真實 R 套件、1,464 個檔裡有 8 個)。不想格式化就寫進[format] exclude。 poly minify [路徑...]:把 JSON/JSONC 就地壓成一行,移除空白與註解。走跟poly fmt同一套 walk 與[format] exclude,所以 CLI 與編輯器命令答案一致。 獨立命令而不是poly fmt的旗標——兩者契約相反,fmt是「符合專案風格」,而沒有 人的風格是一行 40KB。不支援--strict/--format/--fail-on(沒有 findings 可 塑形,也沒有外部工具會缺席),拼對了卻無效的旗標一律拒絕。- 工具解析順序:指定(VSCode 的
poly.tools,其次poly.toml的[tools])→ 專案內工具 → 內嵌引擎 → PATH(主版號相同即可)→ 受管下載。指定版本號時跳過 PATH,直接下載那個版本。
需要 VSCode 1.85 以上。從 Releases 下載:
poly-syntax-highlight-<版本>.vsix— 通用,不分平台。poly-lsp-<平台>-<版本>.vsix— 要挑對平台,內含對應的 poly binary:darwin-arm64(Apple Silicon)、win32-x64、linux-x64(WSL 也用這個)。其他平台沒有 poly-lsp,CLI 請用下方的獨立 binary。
安裝方式:VSCode 側邊欄 Extensions → 右上角 ... → Install from VSIX... →
選檔案 → 重新載入視窗。或用命令列:
code --install-extension poly-syntax-highlight-0.18.21.vsix
code --install-extension poly-lsp-darwin-arm64-0.18.21.vsix之後的版本由 extension 自己下載安裝,裝好只問要不要重新載入視窗——兩個各自照自己的間隔檢查,
哪個先發現就一起更新你已經裝了的那幾個。不想自動更新就關掉 poly.updateCheck.enabled
(只裝 poly-syntax-highlight 的話是 poly.syntax.updateCheck.enabled)。
Extensions 面板齒輪選單裡的「自動更新」勾選框管不到 poly:那是 VSCode 從 Marketplace 更新用的, poly 不在 Marketplace 上,勾了也沒有東西可以更新,而且每次從 VSIX 安裝都會被 VSCode 重設。
設定從一項一行("poly.format.enabled": false)改成一個 poly 區塊(見設定)。
升上來之後,把舊的那幾行的值填進區塊裡對應的項目,再刪掉舊的;poly 不會替你搬。
poly.languageServers 與 poly.languageServerLogs 已經拿掉:poly 只做格式化與 lint,
跳到定義、補全這些語言功能交給各語言自己的 extension(Go 是 golang.go),舊的設定直接刪掉。
poly-editor 在 0.18.3 併進了 poly-lsp。舊的 Poly Editor 要自己解除安裝,否則同一份
功能會跑兩份(兩排 lens、Enter 接兩次清單)。
兩個 extension 在 0.6.0 改了名字(poly-lint → poly-lsp、poly-syntax →
poly-syntax-highlight)。換名字等於換 extension id,所以新版是另一個
extension,只能手動裝。
0.5.0 的更新提示還是會跳,但按下 Install 一定失敗,而且訊息會騙你:
Poly: automatic install failed (Error: release has no asset poly-syntax-0.18.21.vsix). The VSIX files were downloaded — install them manually via "Extensions: Install from VSIX".
其實一個檔都沒下載(它在第一個找不到的 asset 就放棄了),所以「Show Files」按下 去也沒有東西。這段程式碼凍在已安裝的 0.5.0 裡,改不了。照下面手動做:
code --uninstall-extension ricky.poly-lint
code --uninstall-extension ricky.poly-syntax再檢查 settings.json:如果裡面有 "editor.defaultFormatter": "ricky.poly-lint"
(曾經點過 format-on-save 提示的話就會有),要改成 "ricky.poly-lsp"。留著舊
值不會報錯,只會指向一個不存在的 extension,然後格式化安靜地不動作。
不必裝 extension。macOS/Linux:
curl -fsSL https://raw.githubusercontent.com/linzeyan/vscode-syntax/main/install.sh | shWindows(PowerShell):
irm https://raw.githubusercontent.com/linzeyan/vscode-syntax/main/install.ps1 | iex腳本挑對平台、對 SHA256SUMS 驗 sha256、把 binary 放進 ~/.local/bin
(Windows 是 %LOCALAPPDATA%\Programs\poly,並寫進使用者 PATH)。要換位置或釘
版本就設環境變數——irm | iex 沒辦法傳參數,所以兩邊都認得:
POLY_VERSION=0.18.21 POLY_INSTALL_DIR=~/bin sh install.shWindows on ARM 上會裝 arm64 版,即使腳本本身跑在 x64 模擬層裡(從 ssh 或某些
終端機啟動時會發生,此時 PROCESSOR_ARCHITECTURE 說的是 process 不是機器)。
要自己來的話,Release 另附獨立 binary(poly-darwin-arm64、poly-linux-x64、
poly-win32-arm64.exe …):
curl -fsSLO https://github.com/linzeyan/vscode-syntax/releases/latest/download/poly-darwin-arm64
xattr -d com.apple.quarantine ./poly-darwin-arm64 # 瀏覽器下載才需要
chmod +x poly-darwin-arm64 && mv poly-darwin-arm64 /usr/local/bin/polySHA256SUMS 一併發佈,可先驗再用。Windows 上未簽章的 poly.exe 可能被
SmartScreen 擋,處理方式見
extensions/lsp/README.md。
- uses: linzeyan/vscode-syntax@v0
- run: poly check --strict .@v0 會跟著最新的 release 走。要釘死版本就寫 with: { version: "0.18.21" }——poly
會改寫檔案,所以新版本自己跑進來有可能把綠的分支變紅。
Action 做三件事:抓對應平台的 binary、對 SHA256SUMS 驗 sha256、放進 PATH。順便
快取 poly 之後會下載的外部 linter(with: { cache: false } 可關)——冷跑一次
poly check 在 lint 任何東西之前要先抓幾十 MB 的 shellcheck、hadolint。
docker run --rm -v "$PWD:/work" ghcr.io/linzeyan/poly check --strict .linux/amd64 與 linux/arm64 都有。tag 有 latest、0.18.21、0.18;pre-release
不會動到 latest。image 裡的 binary 就是 release 附的那一支,不是另外編的。
image 不含任何語言 toolchain,只含 poly 自己會下載的那些 linter。所以 Rust
專案在容器裡要拿掉 --strict——cargo clippy 只可能來自 toolchain,poly 下載不
到,--strict 會(正確地)把它當成錯誤。同理 poly fmt 在容器裡不會有
clang-format、swift-format、terraform fmt。
外部 linter 快取在 /cache,CI 裡掛個 volume 上去就不用每次重抓:
docker run --rm -v "$PWD:/work" -v poly-cache:/cache ghcr.io/linzeyan/poly check .單一 binary、沒有 runtime 依賴,直接當 hook 用。手寫 .git/hooks/pre-commit:
#!/bin/sh
poly fmt --check --changed || {
echo "run: poly fmt --changed" >&2
exit 1
}
poly check --changed --strict--changed 的範圍是 working tree vs HEAD 加上 untracked,比「只看 staged」寬——用
git add -p 分次 stage 時會檢查到還沒 stage 的改動,是刻意的保守近似。
用 pre-commit framework 的話它自己會把 staged 檔案逐個傳進來,範圍更準:
repos:
- repo: local
hooks:
- id: poly-fmt
name: poly fmt --check
entry: poly fmt --check
language: system
pass_filenames: true
- id: poly-check
name: poly check
entry: poly check --strict
language: system
pass_filenames: truepoly tools # 列出每個外部工具解析到哪裡
poly fmt --check . # 對整個 repo 做 dry-run在編輯器裡開一個 .rs 檔,Developer: Inspect Editor Tokens and Scopes,把游標
放在 -> 上——scope 含 keyword.operator.arrow.skinny.rust 就代表 poly 的文法
生效了(內建文法沒有這個 scope)。
poly fmt <paths...> # 就地格式化
poly fmt --check <paths...> # 只回報,不改檔(CI 用)
poly check <paths...> # 跑 lint
poly check --strict <paths...> # 工具缺席時視為錯誤,而不是跳過(fmt 也吃)
poly fmt --changed # 只處理 git 變更的檔案(pre-commit 用)
poly tools list # 工具解析狀態
poly tools install [tool...] # 預先抓好受管工具(離線環境先在有網路的機器跑)
poly config export # 印出含所有預設值與註解的 poly.toml
poly deadcode [路徑] # 進入點走不到的程式碼(見下)
poly lsp # 給編輯器用的 LSP daemon
poly --help # 完整說明
poly --version # 版本(確認 PATH 上是哪一支)fmt 與 check 共用的旗標裡,這五個值得說明:--format 決定 stdout 的形狀(見
下),--compact 每個問題只印一行,--no-ignore 連 git 忽略的檔案也處理,
--hidden 連點開頭的檔案/目錄也處理,--strict 讓「工具找不到」變成錯誤而不是
跳過該檔。--check 只有 fmt 認得——check 本來就不寫檔,給它 --check 會直接
報錯而不是靜默忽略。
--strict 值得特別說:預設情況下 gofumpt 或 swift-format 沒裝,poly 會在 stderr
說一聲然後跳過那些檔案,exit code 不受影響。這對「不是每台機器都裝了每套
toolchain」是對的預設,但 CI 需要的是相反的答案——--strict 就是那個開關。
跟 poly check 分開,因為它回答的是另一個問題。單檔 linter 問「這個 package/模組裡
有沒有人提到它」,所以匯出的東西永遠不算 unused——外面可能有人用。這個命令問的是
「從進入點有沒有任何路徑會跑到它」,那才是刪掉一段程式碼之前要問的問題。代價是它要花
一次 build 的時間,而且對 library 來說每個匯出的 API 都會是「死的」(呼叫者在別人的
repo 裡)——所以它是你去問的,不是存檔時自動跑的,也不進 CI gate。
四個語言,三支工具,poly 一支都不自己寫(R7/A6):
| 語言 | 工具 | 範圍怎麼決定 |
|---|---|---|
| Go | golang.org/x/tools/cmd/deadcode |
往上找到 go.work 就用它,否則最近的 go.mod |
| TypeScript/JS | knip | 那個 knip 旁邊的 package.json |
| Python | vulture | 最近的 pyproject.toml/setup.py/setup.cfg |
給一個檔案就只跑它那個語言的;給一個目錄,則凡是往上找得到標記的都跑——monorepo 裡 只回答三個語言中的一個,是沒有人要的子集。
Rust 沒有,這是誠實的空白:rustc 自己的 dead_code 已經隨 cargo clippy 每次存檔
就到了,而跨 crate 的那一問沒有主流工具在回答。
Go 的跨 module 靠 go.work:有 go.work 的話分析從 workspace 根開始,liba 裡只被
appb 呼叫的函式就是活的;沒有 go.work,liba 根本不在 build list 裡。這是
Poly: Create go.work for the Open Go Modules 那個命令的第二個用途。
三支工具都跟著各自的 toolchain 走,poly 不代裝:
go install golang.org/x/tools/cmd/deadcode@latest # Go
npm install --save-dev knip # TypeScript/JavaScript
pip install vulture # Python
poly deadcode .vulture 是唯一需要 poly 幫忙的:它自己走目錄,而且不知道 venv 是什麼——直接指給它一個
專案根目錄,會拿到 pip 內建那份 vendored 程式碼的幾千條回報。poly 改成把自己走出來的
檔案清單交給它(同一套 .gitignore 與 [lint] exclude,跟 poly check 一致),再加
一層 --exclude 擋掉 venv/site-packages。
編輯器裡是 Poly: Analyze Dead Code,在終端跑同一行;上面這些語言的每個檔案,第一行
程式碼上面也會有一條 analyze dead code lens(poly.deadCodeCodeLens.enabled 可關)。
不是第 0 行——shebang、版權標頭、//go:build 都在那上面。一個檔一條,不是一個函式一條
——分析本來就是整個 program 的,一個函式一條只是同一個答案的 N 個入口。
poly 預設對任何問題都 exit 1,連 info 等級的錯字也算。要放寬就設嚴重度門檻:
poly check --fail-on warning . # info/hint 照樣回報,但不擋
poly check --fail-on error . # 只有語法錯誤等級才擋
poly check --fail-on never . # 純報告,永遠 exit 0--fail-on=warning 與 --fail-on warning 都認得。低於門檻的問題還是會印出來,
summary 會加註 (N below fail-on),所以綠色的 run 有輸出不會被誤讀成 bug。
四個等級是 poly 的判斷,不是照抄上游工具的:
| 等級 | 意思 |
|---|---|
error |
幾乎確定是缺陷:會壞、不安全,或不合法 |
warning |
可疑但可能是故意的,值得看一眼 |
info |
風格與一致性,不影響正確性 |
hint |
建議與偏好 |
同一份判準套到每個工具,所以 --fail-on error 在 Lua、SQL、Dockerfile、workflow 上
擋的是同一類東西。本來就有等級而且意思相同的工具(shellcheck、clippy、biome、eslint、
swiftlint、selene、tflint、hadolint、arity、rumdl)照用它們自己的;不排序的工具由 poly 排一次
(ruff、golangci-lint、sqruff、deno_lint 是 warning,typos 是 info——deno_lint 把每一條
都印成 error 是它 CLI 的顯示方式,不是分級);poly 自己的規則則是一條規則一個等級。
寫進 poly.toml 才能讓編輯器與 CI 同一套標準,而且兩邊可以不同——「沒格式化要擋,
錯字不用」是很常見的政策:
[format]
fail-on = "warning" # 未格式化是 warning,所以 "error" 等於讓格式化只是建議
[lint]
fail-on = "error" # typos 報 info、多數 linter 報 warning、語法錯誤報 error旗標壓過設定檔。這不是 Rust 的 -D warnings——那個旗標存在是因為 Rust 的
warning 預設不會 fail,poly 是相反的問題。
Exit code:0 乾淨、1 有差異或違規、2 執行錯誤。--help 與 --version 放在
哪個位置都認得(poly fmt --help 跟 poly --help 一樣);--help 走 stdout、
exit 0,指令打錯則是同一份說明走 stderr、exit 2。
說明文字有英文與正體中文兩版,看系統 locale(LC_ALL/LC_MESSAGES/LANG),
POLY_LANG=en 或 POLY_LANG=zh-TW 可強制指定——CI 要讓 log 語言固定時用它。只有
說明文字翻譯:診斷紀錄的格式是給 script 解析的契約,而且訊息有一半來自只講英文的
上游工具,翻譯嚴重度只會弄壞所有消費端。
不管問題是哪個 linter 或 formatter 找到的,fmt 與 check 都印同一種紀錄,走
stdout:
src/app.py:1:8: warning [ruff/F401] `os` imported but unused
fix Remove unused import: `os`
docs https://docs.astral.sh/ruff/rules/unused-import
deploy.sh:4:8: info [shellcheck/SC2086] Double quote to prevent globbing and word splitting.
fix shellcheck can rewrite this automatically
docs https://www.shellcheck.net/wiki/SC2086
schema.sql:1:1: warning [poly/unformatted] file is not formatted
fix run `poly fmt`
第一行是完整紀錄:路徑:行:欄: 嚴重度 [工具/規則] 說明,永遠只有一行且前綴固定,
所以 rg、CI annotation script、終端機的檔案連結都吃得下。後面縮排的 fix/docs
只在該工具真的有給時才出現——多數 linter 只說哪裡錯、把怎麼修留給文件,poly 不替它
們編造。--compact 會把縮排行全部拿掉。
編輯器裡是同一份資訊:fix 併進 Problems 的訊息(LSP 沒有對應欄位),docs 變成
規則代碼上的超連結,用字與 CLI 完全相同。
錯誤(parse 失敗、工具缺席、引擎不接受的設定)也是同一種紀錄,嚴重度 error、規則
poly/format,位置指在 parser 停下來的地方;引擎畫的 code frame 縮排接在後面,一個
問題仍然只佔一行有錨點的輸出。
綠燈有兩種意思——「沒問題」跟「沒人看」。poly check 每次都在 stderr 印一份 coverage
把兩者分開:這次走到的檔案是被誰檢查的、誰沒檢查、為什麼。
coverage:
actionlint 6 files ran
biome 64 files absent — no node_modules/.bin/biome with a biome.json above these files
cargo 25 files ran
deno_lint 27 files ran
eslint 27 files absent — no node_modules/.bin/eslint with an eslint config above these files
hadolint 1 file off-by-default — add `hadolint = "on"` under [tools] to run it as well
poly/actions 6 files ran
poly/docker 1 file ran
ruff 12 files ran
shellcheck 10 files ran
toml 8 files ran
typos 136 files ran
8 tools ran, 0 issues
只列這次有檔案可看的 checker:沒有 Go 的 repo 不會印 golangci-lint。狀態六種:
| 狀態 | 意思 |
|---|---|
ran |
跑過了 |
missing |
poly 找不到這支工具,--strict 會因此 exit 2 |
disabled |
poly.toml 寫了 [tools] <名字> = "off" |
off-by-default |
poly 預設不跑它,= "on" 才會 |
absent |
專案自己沒有(eslint/biome 只用專案裝的那份) |
failed |
跑了但壞了,exit 2 |
files 是 poly 交給它的檔案數,不是它最後讀了幾個——工具自己的設定(_typos.toml
的 exclude、buf.yaml 選的規則)還會再縮一次。Windows 沒有 shellcheck build,所以
Dockerfile 的 RUN 與 workflow 的 run: 在那裡是 missing 而不是靜靜地跳過。
同一份資料在 --format json 的 summary.coverage,四個欄位:tool、files、
status、reason。
--format 只改 stdout 的形狀,不改判定結果——exit code 與 stderr 的 summary
在四種形狀下完全一樣。
| 值 | 用途 |
|---|---|
text |
上面那種紀錄,預設 |
json |
單一份文件,欄位齊全,不必再從文字解析 |
table |
對齊的欄位,掃過去用 |
table_markdown |
貼進 PR 留言或 $GITHUB_STEP_SUMMARY |
poly check --format table .FILE SEVERITY RULE MESSAGE
lint.py:1:8 warning ruff/F401 `os` imported but unused
run.sh:2:6 info shellcheck/SC2086 Double quote to prevent globbing and word splitting.
json 是給 pipeline 的。位置是 1-based(跟紀錄一致),message 完整保留(含引擎畫的
code frame),fix 是跟終端機、編輯器一字不差的同一句話,fatal 直接告訴你這一筆在
當前 --fail-on 下算不算擋——消費端不用重寫嚴重度排序。category 是這是哪一類缺
陷,全部語言共用同一套詞(unpinned-dependency、unused-code、silently-discarded
…),所以一份 pipeline 的 findings 可以照類別分組而不是照工具。poly 自己的規則每條都
有;大部分上游規則是 null——ruff、clippy、eslint 各有數百條而且專案可以自由開關,替
每一條取一個 poly 名字等於再養一套會漂移的規則表。summary.uncategorized 就是這一次
有幾筆沒有類別:
version 只在欄位改變意義或消失時才加,新增欄位不動它。stdout 只有這份文件,
poly check --format json . | jq 不會被 stderr 的 summary 弄髒。
兩種 table 只有四欄,不含 fix/docs——一列必須是一行,而一整欄的 URL 比其他三欄
加起來還寬。table_markdown 把 docs 連結掛在規則名上,不多佔寬度。要完整資訊用
text 或 json。--compact 只對 text 有意義,配其他形狀會直接報錯。
放進 GitHub Actions:
- run: poly check --format table_markdown . >> "$GITHUB_STEP_SUMMARY"poly-lsp 的設定全部在一個 poly 物件裡,extension 啟用時把它整個寫進使用者的
settings.json:每一項上面的註解寫用途、可選的值與預設值(照編輯器的語言,英文或正體中文),
沒設的項目是註解掉的預設值。要改哪一項就取消註解再改值。每次啟用都會依裝的版本重寫註解與
預設值,你設過的值原樣保留,poly 不認得的鍵也留著並註明。
"poly": {
"format": {
// 用途:允許 poly 改寫檔案:Format Document、存檔時格式化、Format Selection……
// 可選:true|false;預設:true
"enabled": false,
},
…
}這份 README 寫 poly.format.enabled 的地方,指的就是 "poly": { "format": { "enabled": … } }。
設定畫面裡 poly 只有一項「在 settings.json 內編輯」;要逐項挑值,用命令 Poly: Settings
(設定畫面那一項的說明裡也有連結):每一列是一項與它目前的值,可以用鍵名、值或用途篩選;
選了就從清單挑值或輸入,挑回預設值等於取消設定,陣列與物件則跳到 settings.json 的那一行。
它寫的就是這個區塊。Format/Lint 開關與
Poly: Set Syntax Color 直接改這個區塊,註解不會掉;settings.json 有沒存檔的修改時它們會請你
先存檔,啟用時的重寫則等下一次。工作區的 .vscode/settings.json 用同樣的巢狀寫法,只寫要覆蓋的
那幾項。
這項是 VSCode 自己的,poly 不改它:
// 編輯器自己的 Find All References 開 peek 還是開內建的 References 面板。預設 "peek";
// 設成 "view" 會和 poly 的 `N refs` CodeLens 一樣留著清單不跑掉——但兩邊是不同的樹,
// 帶行號與符號欄位的是 poly 那棵。
"references.preferredLocation": "view"poly.toml 是選用的。完全沒有設定檔時,語言用內建副檔名表判斷,格式化用各引擎
預設值,走訪檔案時尊重 git 會尊重的忽略檔——.gitignore、.ignore、
.git/info/exclude,以及 core.excludesFile(沒設就是
$XDG_CONFIG_HOME/git/ignore),沿路每一層祖先目錄的都算。跟 git 一樣,全域忽略
檔只在 git repo 裡生效。點開頭的檔案與目錄預設跳過,.github/ 例外(workflow 是原
始碼,actionlint 就是為它接的)。
drawio 與 excalidraw 的存檔(.drawio、.dio、.drawio.svg、.dio.svg、
.excalidraw、.excalidraw.svg、.excalidraw.json)不歸任何語言:編輯器每次存檔都照自己的排版整份重寫,格式化它只會
跟下一次存檔來回改。拼字與 Unicode 也不檢查,抓到的多半是編輯器產生的 id 片段,在檔案
裡改不掉。真的要 poly 管,在 [languages.map] 指定;map 比內建判斷優先而且分大小寫,
所以 "*.json" = "jsonc" 這種寬的 pattern 也會把它們一起接回來。
--no-ignore 關掉前一段的忽略檔,--hidden 讓走訪進入點開頭的路徑;.git/ 兩者
都進不去,物件庫不是原始碼。用在要檢查的正好是被藏起來的東西:generated code、
vendored tree、.config/ 底下的專案腳本。
專案的原始碼本來就住在點開頭目錄時,改用設定檔而不是旗標,這樣編輯器與 CI 看到的 檔案集合才一致:
[walk]
include-hidden = true兩者都只能把範圍放寬、不能收窄;要收窄請用 exclude。poly.toml 的 exclude 不受
--no-ignore/--hidden 影響——那是專案自己說「別碰」,跟 VCS 說「別追蹤」是兩件
事。
要覆蓋預設值時,專案層的真相放 repo 根目錄的 poly.toml,CLI 與 extension 都讀
它,保證編輯器與 CI 行為一致:
[languages.map] # 副檔名 ↔ 語言
"*.tpl" = "jinja"
[format]
exclude = ["vendor/**", "**/*.generated.ts"]
[format.python] # 每語言可調的三個選項
line-width = 100
indent-width = 4
use-tabs = false
[lint]
exclude = ["third_party/**"]
ignore = ["typos/typo", "unused-code"] # 整個 repo 都不報這些
[lint.severity] # 不同意 poly 的等級就改掉,最精確的那條贏
unpinned-dependency = "info"
"poly/docker-latest-base" = "error"
[lint.per-file-ignores] # 只關掉某條規則,檔案照樣 lint
"tests/fixtures/**" = ["ruff/F401"]
"vendor/*.sh" = ["shellcheck/*"] # tool/* 是整支工具
[lint.schemas] # 這些 YAML/TOML 照哪份 JSON Schema 驗證;檔案自己的 $schema 行優先
"compose*.yaml" = "https://json.schemastore.org/docker-compose.json"
"config/*.toml" = "schemas/config.schema.json" # 路徑相對於這個 poly.toml
[walk]
include-hidden = false # 預設;true 會連點開頭的路徑一起走(.git/ 仍然跳過)
[tools] # 指定路徑、釘版本,或設 "off" 關掉;VSCode 裡改用 settings 的 poly.tools,見下
shellcheck = "C:/tools/shellcheck.exe"
tflint = "off"規則代碼就是輸出裡印的那個——看到 [ruff/F401] 就複製 ruff/F401,沒有第二套語法要
查。三個地方(ignore、[lint.severity]、[lint.per-file-ignores])與原始碼裡的註
釋用的是同一套寫法,差別只在範圍:exclude 讓整個檔案不進 lint,ignore 是整個 repo
不報這條,per-file-ignores 只拿掉某個路徑的那一條,註釋只管一行。少了工具名的
"F401" 會讓 poly.toml 解析失敗,而不是安靜地什麼都沒關掉。
也可以寫類別(unused-code、unpinned-dependency…):就是 --format json 那個
category,一句話管到所有語言——今天決定「不看沒用到的程式碼」,明天加進來的語言照樣
算數,不必回頭補 vulture/*。poly config export 印得出完整清單;打錯字會讓解析失敗,
因為類別是 poly 自己的封閉集合,拼錯就永遠對不到任何東西。
[lint.severity] 是專案跟 poly 的等級意見不同時用的——最精確的那條贏,所以類別設
基準、tool/rule 設例外。它同時改終端機印的字、編輯器波浪線的顏色與 fail-on 擋不擋,
三者是同一個決定。編輯器與 CI 讀同一份設定,所以關掉的規則在 Problems 裡也不會出現。
要關掉的只是某一行而不是整個檔案時,把同一組代碼寫成註釋放進原始碼:
import os # poly: ignore ruff/F401# poly: ignore poly/docker-apt-get-unpinned, shellcheck/SC2086
RUN apt-get install -y $PACKAGES註釋管自己這一行;獨佔一整行時再多管下面一行——長行與用 \ 續行的指令沒地方擺行尾
註釋,這個位置就是給它們的。代碼與 tool/* 跟設定檔那邊完全一樣,而且對 poly 跑的
每一支工具都有效,包含下載回來的 shellcheck、actionlint。兩者的分工是:註釋指不到路
徑,設定檔指不到行。
Dockerfile 只認寫在上一行的形式:poly fmt 會把 Dockerfile 的行尾註釋搬到獨立一
行,搬完就變成在管下一道指令,所以寫在行尾的會被報成 poly/ignore-syntax、而且什麼
都不關——與其讓它現在有效、下次格式化後改去關別行,不如當場說清楚。這是唯一有這條規
則的語言,其他語言的 formatter 都會把行尾註釋留在原地。
有 #////-- 行註釋的語言才讀得到;markdown、HTML、CSS 這類沒地方寫,就只剩
per-file-ignores 一條路。註釋裡寫了 poly 讀不懂的代碼會以 poly/ignore-syntax 報出
來,而不是讓整個 run 中斷——一個檔案裡的一行註釋不值得讓整個 repo 停下來,而且它原
本想關掉的那條 finding 還是照樣印在旁邊。各工具自己的 # noqa、
# shellcheck disable=、-- selene: allow(...) 一律照舊有效,poly 不碰。
例外是 # hadolint ignore=:hadolint 預設關閉,這行註釋就什麼都關不掉了。poly 會把
它報成 poly/ignore-syntax,並直接告訴你該改寫成哪一行——例如
# poly: ignore poly/docker-apt-get-unpinned。poly 不會去解讀 hadolint 的語法,
這是遷移提示不是相容層:講一次,讓你把註釋換掉然後刪了它。把 hadolint 開回來
([tools] hadolint = "on")的話這行註釋照常有效,poly 也就不再提。
用 VSCode 的話,[tools] 這張表改寫在 settings.json 的 poly.tools,鍵與值都一樣
("off"、"on"、版本號、路徑;路徑請寫絕對路徑),不必為了釘一個版本在 repo 裡多放一個
poly.toml:
"poly": {
"tools": {
"shellcheck": "off",
"tflint": "0.53.0",
},
}extension 啟動的每個 poly 都會套用:daemon(poly lsp)、PlantUML 的 jar、終端機裡的 poly check
與 dead code 分析;改了會自動重啟 daemon。同一個工具兩邊都有寫時以 settings 為準,其他
工具照 poly.toml。純命令列(包括 CI)不讀 settings,只讀 poly.toml——要讓 CI 也用同一個
版本,就寫在 poly.toml。
[format.<lang>] 只認 line-width(1-1000)/indent-width(1-16)/use-tabs
三個鍵,拼錯或超出範圍都會直接讓解析失敗而不是靜默忽略;只作用於內嵌引擎,走外部
工具的語言請用該工具自己的設定檔。VSCode settings 放個人偏好
(poly.serverPath、poly.lintOnSave、poly.updateCheck.*)與上面的 poly.tools。
專案已經有 .editorconfig 的話什麼都不用做:內嵌引擎會沿用它的 indent_style/
indent_size/max_line_length,這三個鍵剛好就是上面那三個 knob,不是另一套要維護
的設定表面。[format.<lang>] 寫過的鍵逐鍵壓過它,所以 poly.toml 設 line-width、
.editorconfig 設縮排時兩邊都算數。引擎吃不下的值(例如 XML 沒有 line-width)在
這裡是安靜丟掉,而不像寫在 poly.toml 裡會讓解析失敗——.editorconfig 是寫給這個 repo
用過的每一個編輯器看的,不是寫給 poly 的,為了它拒絕格式化整個專案只會讓人以為是
poly 壞了。走外部工具的語言不經過這條路,那些工具自己就會讀 .editorconfig。
編輯器那半也一併沿用:打字時的 tab 寬度、存檔時的行尾空白與檔尾換行、行尾字元,
連 poly 不格式化的檔案(.ini、Makefile……)都算。charset 與 max_line_length
不處理。
專案之外還有一份全域的:~/.config/poly/poly.toml(設了 XDG_CONFIG_HOME 就在它底下;
Windows 是 %APPDATA%\poly\poly.toml),命令列與編輯器都讀。它在最底層,專案裡的 poly.toml
逐鍵壓過它,而且它永遠不算專案根目錄。poly lsp 啟動時發現它不存在,就用 poly config export
寫一份完整的;之後只要裡面的值都還是預設值,就跟著新版重寫,改過任何一個值就不再動它。
完整的鍵、可填的值、每個引擎的預設值都寫在
poly.example.toml 裡。那份檔案是 poly config export 產生
的,工具名稱、pin 住的版本、語言清單都直接讀自 binary,所以不會跟你手上這一版
poly 說的不一樣;想拿當下這支 binary 的版本就跑 poly config export > poly.toml,
整份存下來不改任何一行也不會改變 poly 的行為。
cargo build --release --manifest-path cli/Cargo.toml # → cli/target/release/poly
(cd extensions/lsp && pnpm install && pnpm run build) # extension bundle
(cd extensions/lsp && pnpm test) # 真 extension host E2E
pip install pyyaml && python tools/grammar-sync.py # 重新同步文法
python tools/tool-sync.py --check # 驗證外部工具 pin(離線)
python tools/tool-sync.py --update # 跟上游對一次版本(需網路).vscode/settings.json 已把 poly.serverPath 指向 cli/target/release/poly,
所以在本 repo 裡按 F5 就會用剛建好的 binary。
poly 自身的程式碼為 MIT,全文見 LICENSE(一併附在每個 release、兩個
VSIX 與 container image 裡)。內嵌文法與相依套件各自保留上游授權,完整清單見兩份
THIRD-PARTY-NOTICES.md(由同步管線自動產生,含 pinned 版本)。授權允許清單由
tools/grammar-sync.py 與 tools/third-party-notices.py 在 CI 強制執行:
permissive 授權加 MPL-2.0,GPL/AGPL/SSPL 與無 permissive 選項的 LGPL 一律擋下。
{ "version": 2, "command": "check", "issues": [ { "file": "lint.py", "line": 1, "col": 8, "end_line": 1, "end_col": 10, "severity": "warning", "tool": "ruff", "rule": "F401", "category": null, "message": "`os` imported but unused", "fix": "Remove unused import: `os`", "docs": "https://docs.astral.sh/ruff/rules/unused-import", "fatal": true } ], "summary": { "issues": 1, "fatal": 1, "uncategorized": 1, "coverage": [ { "tool": "ruff", "files": 12, "status": "ran", "reason": null }, { "tool": "shellcheck", "files": 4, "status": "missing", "reason": "shellcheck has no managed build for win-x64 and is not on PATH" } ] } }