Slide 1

Slide 1 text

Exploring Type-Informed Lint Rules in Rust based TypeScript Linters 1 Rust製TypeScript Linterにおける 型情報Lintルールの模索 TSKaigi 2024 @unvalley_

Slide 2

Slide 2 text

- Rust / TypeScript - A core member of Biome - Interested in Developer Tools 2 @unvalley_

Slide 3

Slide 3 text

3 typescript-eslint ESLint

Slide 4

Slide 4 text

4 typescript-eslint ESLint

Slide 5

Slide 5 text

typescript-eslint 5 1. ESLintへのTypeScript構文のサポート

Slide 6

Slide 6 text

6 1. ESLintへのTypeScript構文のサポート 2. TypeScriptの型情報を用いたLintルール typescript-eslint

Slide 7

Slide 7 text

7 1. ESLintへのTypeScript構文のサポート 2. TypeScriptの型情報を用いたLintルール 型の不整合や実行時エラーの可能性がある コードパターンに対して静的検査(Lint)を行う typescript-eslint

Slide 8

Slide 8 text

8 typescript-eslintの型情報Lintルール https://typescript-eslint.io/rules/?=typeInformation

Slide 9

Slide 9 text

9 TypeScriptの型情報を用いたLintルール(例) - switch-exhaustiveness-check - strict-boolean-expressions - no-floating-promises

Slide 10

Slide 10 text

10 TypeScriptの型情報を用いたLintルール(例) - switch-exhaustiveness-check - strict-boolean-expressions - no-floating-promises

Slide 11

Slide 11 text

@typescript-eslint/switch-exhaustiveness-check 11 union型・enumが、switch文で全ケースを網羅しているか検査するルール type Day = 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday'; declare const day: Day; let result = 0; switch (day) { case 'Monday': result = 1; break; }

Slide 12

Slide 12 text

@typescript-eslint/switch-exhaustiveness-check 12 union型・enumが、switch文で全ケースを網羅しているか検査するルール type Day = 'Monday' | 'Tuesday' | 'Wednesday' | 'Thursday' | 'Friday' | 'Saturday' | 'Sunday'; declare const day: Day; let result = 0; switch (day) { // ❌ ^^^ case 'Monday': result = 1; break; } Switch is not exhaustive. Cases not matched: "Tuesday" | "Wednesday" | "Thursday" | "Friday" | "Saturday" | "Sunday" > Add branches for missing cases.

Slide 13

Slide 13 text

13 TypeScriptの型情報を用いたLintルール(例) - switch-exhaustiveness-check - strict-boolean-expressions - no-floating-promises

Slide 14

Slide 14 text

@typescript-eslint/strict-boolean-expressions 14 boolean 式で特定の型を許可しないルール // nullable strings are considered unsafe let str: string | null = null; if (!str) { console.log('str is empty'); }

Slide 15

Slide 15 text

@typescript-eslint/strict-boolean-expressions 15 boolean 式で特定の型を許可しないルール // nullable strings are considered unsafe let str: string | null = null; if (!str) { // ❌ ^^^^^ console.log('str is empty'); } Unexpected nullish value in conditional. The condition is always false.

Slide 16

Slide 16 text

Tips: strict-boolean-expressionsをさらに厳しくする 16 boolean 式で特定の型(string, number, nullableObjects)を許可しないルール "@typescript-eslint/strict-boolean-expressions": [ "error", { "allowString": false, "allowNumber": false, "allowNullableObject": false } ]

Slide 17

Slide 17 text

17 TypeScriptの型情報を用いたLintルール(例) - switch-exhaustiveness-check - strict-boolean-expressions - no-floating-promises

Slide 18

Slide 18 text

@typescript-eslint/no-floating-promises 18 Promise を返す関数・メソッドが、適切にハンドルされているか検査するルール function returnPromise() { return new Promise((resolve, reject) => { setTimeout(() => resolve("data"), 1000); }); } returnPromise(); // ? returnPromise() // ? .then(data => console.log(data)) .catch(err => console.error(err));

Slide 19

Slide 19 text

@typescript-eslint/no-floating-promises 19 Promise を返す関数・メソッドが、適切にハンドルされているか検査するルール function returnPromise() { return new Promise((resolve, reject) => { setTimeout(() => resolve("data"), 1000); }); } returnPromise(); // ? returnPromise() // ? .then(data => console.log(data)) .catch(err => console.error(err)); エラー処理されていない状態で作成される Promise のこと。 「操作順序が不適切になること」や「Promise rejectionsの無視」など の問題を引き起こす可能性がある

Slide 20

Slide 20 text

@typescript-eslint/no-floating-promises 20 Promise を返す関数・メソッドが、適切にハンドルされているか検査するルール function returnPromise() { return new Promise((resolve, reject) => { setTimeout(() => resolve("data"), 1000); }); } // ❌ returnPromise(); ^^^^^^^^^^^^^^^ // ✅ returnPromise() .then(data => console.log(data)) .catch(err => console.error(err)); Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the void operator.

Slide 21

Slide 21 text

21 Tips in typescript-eslint : strictTypeChecked strictTypeChecked > recommendedTypeChecked > recommended // eslint.config.js export default tseslint.config( ...tseslint.configs.strictTypeChecked ); https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/src/configs/strict-type-checked.ts

Slide 22

Slide 22 text

22 typescript-eslint ESLint

Slide 23

Slide 23 text

Rust製TypeScript Linter 23

Slide 24

Slide 24 text

Rust製TypeScript Linterの利用拡大 24 Biome stars : 10.8k npm downloads : 296,728 (weekly)

Slide 25

Slide 25 text

 Biome - One Toolchain for Your Web Project - パフォーマンスに優れたRust製のWebツールチェーン - Linterは、200以上のルールを持つ(JS/TS/JSX/CSS) - Formatterは、Prettier互換(97% in JS/TS/JSX) - スポンサー(募集中!) - 時雨堂, トヨクモ, 要, nanabit, vital, GitHub sponsors - Open Collective : https://opencollective.com/biome 25

Slide 26

Slide 26 text

Rust製TypeScript Linterの利用拡大 26 Biome stars : 10.8k npm downloads : 296,728 (weekly) Oxc (oxlint) stars : 9.0k npm downloads : 49,930 (weekly)

Slide 27

Slide 27 text

Rust製TypeScript Linterの利用拡大 27 Biome stars : 10.8k npm downloads : 296,728 (weekly) Oxc (oxlint) stars : 9.0k npm downloads : 49,930 (weekly) Deno (deno_lint) stars : 93k (1.5k) npm downloads : N/A

Slide 28

Slide 28 text

Rust製TypeScript Linterの利用拡大 28 Biome stars : 10.8k npm downloads : 296,728 (weekly) Oxc (oxlint) stars : 9.0k npm downloads : 49,930 (weekly) Deno (deno_lint) stars : 93k (1.5k) npm downloads : N/A

Slide 29

Slide 29 text

TypeScriptの型情報を用いたLintルールの実装がない 29 Rust製TypeScript Linterの課題

Slide 30

Slide 30 text

TypeScriptの型情報を用いたLintルールの実装がない 30 Rust製TypeScript Linterの課題 typescript-eslintでは、どのように実装されている?

Slide 31

Slide 31 text

31 1. Source Code 2. Lex 3. Parse 4. AST 5. Lint(型情報が不要なLintを実行可能)

Slide 32

Slide 32 text

32 1. Source Code 2. Lex 3. Parse 4. AST 5. Lint(型情報が不要なLintを実行可能)

Slide 33

Slide 33 text

33 1. Source Code 2. Lex 3. Parse 4. AST 5. Lint(型情報が不要なLintを実行可能) 6. Query Type Information(型情報の取得) 7. AST and Type Information(ASTと型情報) 8. Type Informed Lint(型情報Lintを実行可能)

Slide 34

Slide 34 text

1. Source Code 2. Lex 3. Parse 4. AST 5. Lint(型情報が不要なLintを実行可能) 6. Query Type Information(型情報の取得) 7. AST and Type Information(ASTと型情報) 8. Type Informed Lint(型情報Lintを実行可能) 34

Slide 35

Slide 35 text

35 Query Type Information AST and Type Information Type Informed Lint - NodeへのVisit typescript-eslintが作るASTを使って、関数呼び出しの NodeにVisitする - Promiseの検出 作成したASTを使って、構文的にPromiseを返してそうな関 数・メソッド呼び出しに該当する Nodeを特定し、そのNodeに 該当するTS Compiler APIのASTのNodeを特定して、型情 報を取得する - Promiseのハンドリング確認 検出されたPromiseが適切にハンドルされているか (then, catch, finally のいずれかで処理されているか、または await されてい るか)を型情報を利用して確認する - 型情報Lintルール実行(診断) Promiseが適切にハンドルされていない場合、 Error または Warnを表示する ざっくり@typescript-eslint/no-floating-promises

Slide 36

Slide 36 text

Rust製Linterで型情報Lintルールがない理由 36

Slide 37

Slide 37 text

Rust製Linterで型情報Lintルールがない理由 37

Slide 38

Slide 38 text

Rust製Linterで型情報Lintルールがない理由 38 パフォーマンス Rust製Linterの強みは、Lint実行の高速性。 型情報のためにtsserverにアクセスすると、高速性担保が難しくなる

Slide 39

Slide 39 text

Rust製Linterで型情報Lintルールがない理由 39 パフォーマンス Rust製Linterの強みは、Lint実行の高速性。 型情報のためにtsserverにアクセスすると、高速性担保が難しくなる https://github.com/JoshuaKGoldberg/dot-com/pull/143#discussion_r1440618448 totalLintingTime = Math.max(standardLinting, typeInformedLinting)

Slide 40

Slide 40 text

Rust製Linterで型情報Lintルールがない理由 40 実装コスト(特にパフォーマンスを考慮する場合 ) 型情報を取得する際に、tsserverに依存しないことを考えると、Alternative TypeScript Compilerの実装やサブセットの実装が必要 パフォーマンス Rust製Linterの強みは、Lint実行の高速性。 型情報のためにtsserverにアクセスすると、高速性担保が難しくなる

Slide 41

Slide 41 text

Rust製Linterで型情報Lintルールがない理由 41 パフォーマンス Rust製Linterの強みは、Lint実行の高速性。 型情報のためにtsserverにアクセスすると、高速性担保が難しくなる 実装コスト(特にパフォーマンスを考慮する場合 ) 型情報を取得する際に、tsserverに依存しないことを考えると、Alternative TypeScript Compilerの実装やサブセットの実装が必要

Slide 42

Slide 42 text

Rust製TypeScript Linterが、型情報を取得する方法 42

Slide 43

Slide 43 text

Rust製TypeScript Linterが、型情報を取得する方法 43 1. TypeScript Compilerの利用 2. Alternative TypeScript Compilerの利用 3. Type Inference(サブセット)の実装

Slide 44

Slide 44 text

Rust製TypeScript Linterが、型情報を取得する方法 44 1. TypeScript Compilerの利用 2. Alternative TypeScript Compilerの利用 3. Type Inference(サブセット)の実装

Slide 45

Slide 45 text

Rust製TypeScript Linterが、型情報を取得する方法 45 1. TypeScript Compilerの利用 2. Alternative TypeScript Compilerの利用 3. Type Inference(サブセット)の実装

Slide 46

Slide 46 text

Oxc : 型情報lintルール実装(no-floating-promises) 46 https://github.com/oxc-project/oxc/pull/2912 (数日前にcloseされた)

Slide 47

Slide 47 text

47 Oxc : 型情報lintルール実装(no-floating-promises) Biome Maintener である ty(togami2864)さん提供

Slide 48

Slide 48 text

Rust製TypeScript Linterが、型情報を取得する方法 48 1. TypeScript Compilerの利用 2. Alternative TypeScript Compilerの利用 3. Type Inference(サブセット)の実装

Slide 49

Slide 49 text

Rust製TypeScript Linterが、型情報を取得する方法 49 1. TypeScript Compilerの利用 2. Alternative TypeScript Compilerの利用 3. Type Inference(サブセット)の実装 複雑な型システム・(v1.8以降)仕様書が無い・Microsoftの資本力への追従

Slide 50

Slide 50 text

stc (dudykr/stc) 50 - 高速なTypeScript Type Checker - swcでLex / Parseを行い、stcでType Checkを行う - tscの挙動を仕様として実装が行われており、独自の構文拡張は未実装 - 2024年5月現在、開発は中止されている😢 - “Closing as the stc is now abandoned. TypeScript was not something that I could follow up on in an alternative language.” github.com/swc-project/swc/issues/571 swcのkdy1氏が開発を進めていたRust製のAlternative TypeScript Compiler

Slide 51

Slide 51 text

ezno (kaleidawave/ezno) 51 kaleidawave氏が開発しているRust製のAlternative TypeScript Compiler

Slide 52

Slide 52 text

ezno (kaleidawave/ezno) 52 kaleidawave氏が開発しているRust製のAlternative TypeScript Compiler - Rust製のJavaScript CompilerとTypeScript Checkerで、静的解析と実行時の パフォーマンスに重点を置く - TypeScriptの型注釈を理解し、JavaScriptに対しても型チェックを行う - ソースコードから最大限の知識を得るアイデアに積極的で、無効なプロパティの特 定・デッドコードの検出も行う(Linter的な振る舞い) - stcとは異なり、TypeScript との1-to-1の代替は目指していない

Slide 53

Slide 53 text

Rust製TypeScript Linterが、型情報を取得する方法 53 1. TypeScript Compilerの利用 2. Alternative TypeScript Compilerの利用 3. Type Inference(サブセット)の実装

Slide 54

Slide 54 text

Type Inference(サブセット)の実装 54 - TypeScriptのType Inference(型推論)のサブセット実装を行う - Linterが生成するASTで型情報を含められるなら、外部ツールとの連携を考え ずに済むのでパフォーマンス(速度)や複雑さの観点で嬉しい - 近いツールだと Ruff(Python Tools) や eznoなどが行なっている - ただ当然、Generics, Conditional Types, Recursive Types, Mapped Types, Utility Typesなどよく使われる高度な型推論の対応も必要になる

Slide 55

Slide 55 text

Announcing TypeScript 5.5 Beta 55 https://devblogs.microsoft.com/typescript/announcing-typescript-5-5-beta/

Slide 56

Slide 56 text

--isolatedDeclarations 56 - export対象のメンバーに十分な型注釈を付けるようにして、サードパー ティーツールが型定義ファイルを生成できるようにする機能 - 十分な型注釈がない場合(型検査なしでmoduleを確実に変換できない場 合)にはエラーを出す - TypeScript Playground で、Versionを5.5.0 beta+で試せる https://github.com/microsoft/TypeScript/issues/47947

Slide 57

Slide 57 text

--isolatedDeclarations 57 - export対象のメンバーに十分な型注釈を付けるようにして、サードパー ティーツールが型定義ファイルを生成できるようにする機能 - 十分な型注釈がない場合(型検査なしでmoduleを確実に変換できない場 合)にはエラーを出す - TypeScript Playground で、Versionを5.5.0 beta+で試せる https://github.com/microsoft/TypeScript/issues/47947

Slide 58

Slide 58 text

58 // All cases are errors with --isolatedDeclarations // ❌ Expressions must not use information outside the expression export const nBad = Math.random(); export const lOk = 1; // ❌ Expressions must not reference other variables export const refConst = lOk; // ❌ Inferred return types are not supported export function noReturn(a: number) { } // ❌ Spread operators and mutable arrays are not supported export const newPoint = { ...point }; export const values = [1, 2, 3]; https://github.com/microsoft/TypeScript/pull/58201

Slide 59

Slide 59 text

59 // ✅ All cases are OK with --isolatedDeclarations export const nOk: number = Math.random(); export const lOk = 1; export function explicitReturnOk(a: number): void { } export const colors = { 'blue': 0x0000FF, 'red': 0xFF0000, 'green': 0x00FF00, } as const export const directions = ["UP", "DOWN", "LEFT", "RIGHT"] as const export const component = (props: { name: string }): string => ""; https://github.com/microsoft/TypeScript/pull/58201

Slide 60

Slide 60 text

--isolatedDeclarationsによって何が得られるか? 60 より高速な宣言ファイル生成ツール 型推論を必要としないTypeScriptファイルが得られるため、宣言ファイルの生成が 簡 単になる 並列宣言ファイル生成・並列型検査 巨大なモノレポ環境などで、各プロジェクトが依存関係の宣言ファイルをチェックす るために依存順序でビルドする必要があったが、 宣言ファイルが並列して生成できれば、型検査も並列して実行できる

Slide 61

Slide 61 text

--isolatedDeclarationsと型情報Lintルールの関係性 61 - 型注釈は型情報なので、明記されていれば、Linterが楽に型を知ることが できる - 例えばBiomeでも型が構文から分かるなら、型情報Lintルールの部分的な実 装を行なっている, e.g, useExportType - TSファイルと型定義ファイルが1対1対応する状態になるなら、型定義ファイ ルがある場合に静的な型情報の取得が容易になるはず

Slide 62

Slide 62 text

62 // Normal TypeScript // a.ts ----------------------------------------------------- export function returnPromise() { // 返り値の型を推論する必要がある return new Promise((resolve, reject) => { setTimeout(() => resolve("data"), 1000); }); } // b.ts ----------------------------------------------------- import { returnPromise } from “a”; returnPromise(); // ? returnPromise() // ? .then(data => console.log(data)) .catch(err => console.error(err));

Slide 63

Slide 63 text

63 // with --isolatedDeclarations // a.ts --isolatedDeclarationsが、ReturnTypeを明示------------ export function returnPromise(): Promise { // 型注釈 return new Promise((resolve, reject) => { setTimeout(() => resolve("data"), 1000); }); } // b.ts ---------------------------------------------------- import { returnPromise } from “a”; // returnPromiseの型推論は不要 returnPromise(); // ❌ returnPromise() // ✅ .then(data => console.log(data)) .catch(err => console.error(err));

Slide 64

Slide 64 text

64 // with --isolatedDeclarations // a.ts --isolatedDeclarationsが、ReturnTypeを明示------------ export function returnPromise(): Promise { // 型注釈 return new Promise((resolve, reject) => { setTimeout(() => resolve("data"), 1000); }); } // b.ts ---------------------------------------------------- import { returnPromise } from “a”; // returnPromiseの型推論は不要 returnPromise(); // ❌ returnPromise() // ✅ .then(data => console.log(data)) .catch(err => console.error(err)); JsFunctionDeclaration { ... return_type_annotation: TsReturnTypeAnnotation { ty: TsReferenceType { name: JsReferenceIdentifier { value_token: [email protected] "Promise", }, type_arguments: TsTypeArguments { ts_type_argument_list: TsTypeArgumentList [ TsStringType { string_token: [email protected] "string", }, ], ...

Slide 65

Slide 65 text

Faster TypeScript builds with --isolatedDeclarations 65 https://portal.gitnation.org/contents/faster-typescript-builds-with-isolateddeclarations

Slide 66

Slide 66 text

余談:Slow Types in JSR 66 https://jsr.io/docs/about-slow-types

Slide 67

Slide 67 text

余談:Slow Types in JSR 67 TypeScript内で、以下に当てはまる型 - 明確に定義されていない型 - 推論に広範な処理が必要なほど複雑な型 --isolatedDeclarations と似た概念

Slide 68

Slide 68 text

余談:Slow Types in JSR 68 TypeScript内で、以下に当てはまる型 - 明確に定義されていない型 - 推論に広範な処理が必要なほど複雑な型 型検査の高速化 / ドキュメント・型定義ファイル生成の安定化 https://github.com/jsr-io/jsr/issues/444#issuecomment-2079772908 型注釈によって明示して除去 --isolatedDeclarations と似た概念

Slide 69

Slide 69 text

Rust製TypeScript Linterが、型情報を取得する方法 69 1. TypeScript Compilerの利用 2. Alternative TypeScript Compilerの利用 3. Type Inference(サブセット)の実装

Slide 70

Slide 70 text

型情報の信頼性(安全性)/ Lint実行速度(TypeScript Compilerへの依存) 70 ⚖

Slide 71

Slide 71 text

Rust製TypeScript Linter(型情報Lintルール)の今後の可能性 71 Type Inference のサブセット実装 --isolatedDeclarationsが活用できると良い Type Inference ?(tsserverを利用した型情報の取得 ) eznoとの再連携も今後の可能性としては考えられる 2024年5月現在、保留されている印象 no-floating-promisesは、#13376 “async / await: nowait keyword?” の解決に委ねる?

Slide 72

Slide 72 text

async/await: nowait keyword? no-floating-promises in TypeScript 72 https://github.com/microsoft/TypeScript/issues/13376

Slide 73

Slide 73 text

73 - 2017年当時の課題感: 非同期関数内において、他の非同期関数を呼び出す際のawaitキーワードの付 け忘れを特定するのが難しい。nowaitキーワードも追加して、どちらかが絶対必 要にできるCompiler Optionsが欲しい - 総意をまとめると「 @typescript-eslint/no-floating-promise と似た検出機能を TypeScriptのCompiler Optionとして提供する」になりそう 2017年1月10日に開かれたissue async/await: nowait keyword? no-floating-promises in TypeScript

Slide 74

Slide 74 text

Appendix 74

Slide 75

Slide 75 text

Reconstructing TypeScript 75 https://jaked.org/blog/2021-09-07-Reconstructing-TypeScript-part-0

Slide 76

Slide 76 text

TypeScript Compiler Notes 76 https://github.com/microsoft/TypeScript-Compiler-Notes

Slide 77

Slide 77 text

まとめ:Rust製Linterにおける型情報Lintルールの模索 77 - 型情報Lintルールは、コードの安全性を高める上で重要だが、 TypeScriptの型検査が必要なためLint実行時間を遅くさせてしまう - Rust製TypeScript Linterの型情報Lintルールの実装方法を紹介 1. TypeScript Compilerの利用 2. Alternative TypeScript Compilerの利用 3. Type Inference(サブセット)の実装 (with --isolatedDeclarations) - 今後のRust製TypeScript LinterとTypeScriptの動向を要チェック

Slide 78

Slide 78 text

Acknowledgement 78 - ty and nissy-dev from Biome - sosukesuzuki from Prettier - All Biome (Rome), oxc, deno_lint, ESLint and typescript-eslint contributors - Especially, ematipico, conaclos, Boshen

Slide 79

Slide 79 text

References 79 - Rust-Based JavaScript Linters: Fast, But No Typed Linting Right Now https://www.joshuakgoldberg.com/blog/rust-based-javascript-linters-fast-but-no-typed-linti ng-right-now/ - Rewriting TypeScript in Rust? You'd have to be... | Total TypeScript https://www.totaltypescript.com/rewriting-typescript-in-rust - Let's Make a Generic Inference Algorithm by Ryan Cavanaugh - GitNation https://portal.gitnation.org/contents/lets-make-a-generic-inference-algorithm - Tour de Source: TypeScript ESLint - Sourcegraph https://sourcegraph.com/notebooks/Tm90ZWJvb2s6MTA2OA== - Rust製TypeScriptコンパイラstcの現状と今後 | メルカリエンジニアリング https://engineering.mercari.com/blog/entry/20230606-b059cd98c3/