Slide 1

Slide 1 text

Tomoya Chiba (@tomoasleep) ts-morph と ast-grep でたくさんの TypeScript コ ードを書き変えた話 1

Slide 2

Slide 2 text

千葉 知也 (@tomoasleep) Qiita 株式会社 エンジニア Tokyo と Nagoya に拠点があります 自分は Tokyo の民です Ruby と TypeScript と HCL と YAML を書いたり消した りする仕事をしています 自己紹介 2

Slide 3

Slide 3 text

ライブラリ移行 → たくさん書き換えないと行けない → つらい!!無理!! ts-morph と ast-grep を使って ↑ に立ち向かう話 今日のお題 3

Slide 4

Slide 4 text

移行したもの: GraphQL のコード生成ライブラリ apollo-tooling (Deprecated) → GraphQL Code Generator type User implements Node { id: ID! username: String! email: String! } query FindUser($id: ID!) { user(id: $id) { id username email } } 今回移行したライブラリ 4

Slide 5

Slide 5 text

移行したもの: GraphQL のコード生成ライブラリ apollo-tooling (Deprecated) → GraphQL Code Generator // こんな感じのコードを生成してくれる export interface User = Node & { __typename?: 'User'; id: ID; username: string; email: string; }; export interface GetUser { user: GetUser_user; } export interface GetUser_user { id: string; username: string; email: string; 今回移行したライブラリ 5

Slide 6

Slide 6 text

移行したもの: GraphQL のコード生成ライブラリ apollo-tooling (Deprecated) → GraphQL Code Generator 今回移行したライブラリ 6

Slide 7

Slide 7 text

生成されるものがめちゃくちゃ変わってしまう ( ファイル名, export, ...etc) これをほとんどのコードに対して行う必要がある import GetItemQuery from '~/graphql/generated/someQueryByQueryName.gql' import { GetItem, GetItemVariable, GetItem_items } from '~/graphql/generated/types/someQueryB const { data, refetch } = useQuery(GetItemQuery) const items: GetItem_items[] = data.items ↓ import { GetItem, GetItemDataDocument } from '~/graphql/generated/some-query-by-file-name' type GetItem_items = GetItem['items'][number] const { data, refetch } = useQuery(GetItemDocument) const items: GetItem_items[] = data.items 色々書き換える必要が出てくる 7

Slide 8

Slide 8 text

(VSCode の機能とか頑張って使えばなんとかなるんじゃね?) 結論: 失敗 人がやるとミスが出る 最初の試行錯誤した部分と、その後で書き換え方がブレて一貫性がない 後から発生する要望、フィードバックに答えられない Conflict への対応が重すぎ レビュアーの負担も重い Try 1: 手で頑張って置き換えてみる 8

Slide 9

Slide 9 text

ts-morph, ast-grep 等のツールを使う 全ての書き換えをスクリプト化する 手での書き換えは ( 基本) やらない # 書き換えに使ったスクリプト例 ( 後のスライドで再掲します) # コードの状態をリセット BASE_COMMIT_ID=$(git log --grep="XXXXXXX" --author="tomo.asleep" -n 1 --pretty=%H) git restore --source=$BASE_COMMIT_ID --worktree -- src/ # コードを書き換え ast-grep --pattern 'useQuery<$$$>($$$P)' --rewrite 'useQuery($$$P)' \ --lang ts --update-all src/ ts-node ./rewriter-with-ts-morph.ts # Linter, Formatter でコードを整形 yarn run format Try 2: 自動化する 9

Slide 10

Slide 10 text

https://ts-morph.com/ TypeScript ファイルの書き換えを行える AST 操作だけでなく、様々な書き換え機能を提供 VSCode のリファクタリング機能と出来ることは大体行える ( と思う) 公式ドキュメントが分かりやすい ts-morph 10

Slide 11

Slide 11 text

import { Project } from "ts-morph"; const project = new Project(); const myCode = project.getSourceFile("myCode.ts"); // import MyClass from "./file"; を追加 myCode.addImportDeclaration({ defaultImport: "MyClass", moduleSpecifier: "./file", }): // type TypeAlias = string; を追加 myCode.addTypeAlias({ name: "TypeAlias", type: "string", }); await myCode.save(); ts-morph でコードの追加が簡単に行える 11

Slide 12

Slide 12 text

定義、import した変数とその利用箇所をまとめて rename 出来る // ts-morph で import を書き換える const importDeclaration = myCode.getImportDeclaration("package") const namedImports = importDeclaration.getNamedImports() namedImports.forEach(namedImport => { // import 名と、利用箇所をまとめて rename する namedImport.setName("NewName") }) /* ----------------------------------------------------------------------------- */ // Before import { OldName } from "package" someFunction(OldName) // ↓ // After import { NewName } from "package" someFunction(NewName) ts-morph で複雑な書き換えが簡単に行える 12

Slide 13

Slide 13 text

使ってない import の削除も簡単に出来る sourceFile.fixUnusedIdentifiers(); /* ----------------------------------------------------------------------------- */ // Before import { OldName, NewName } from "package" someFunction(NewName) // ↓ // After import { NewName } from "package" someFunction(NewName) ts-morph で複雑な書き換えが簡単に行える 13

Slide 14

Slide 14 text

https://ast-grep.github.io/ AST を加味した grep, sed, awk みたいなもの 様々な言語 (TypeScript, Ruby, Python, Go, Rust, ...etc) に対応 直感的に書きやすい && 何をやっているかわかりやすい シンプルな書き換えはこちらを使った ast-grep --pattern 'useQuery<$, $>($$$A)' --rewrite 'useQuery($$$A)' \ --lang ts --update-all src/ // Before const data = useQuery(FetchDataQuery) // ↓ // After const data = useQuery(FetchDataQuery) ast-grep 14

Slide 15

Slide 15 text

コードをリセット → 書き換え → Linter 等で整形、を順に行うスクリプトを書く 冪等になるようにスクリプト化しておくと便利 書き換えの試行錯誤がしやすくなる Conflict 解消、変更への追従が簡単 # コードの状態をリセット BASE_COMMIT_ID=$(git log --grep="XXXXXXX" --author="tomo.asleep" -n 1 --pretty=%H) git restore --source=$BASE_COMMIT_ID --worktree -- src/ # コードを書き換え ast-grep --pattern 'useQuery<$$$>($$$P)' --rewrite 'useQuery($$$P)' \ --lang ts --update-all src/ ts-node ./rewriter-with-ts-morph.ts # Linter, Formatter でコードを整形 yarn run format 書き換えをスクリプト化した 15

Slide 16

Slide 16 text

書き換えスクリプトは1 日で作れた (※ 個人差があります) ts-morph, ast-grep 公式ドキュメントが充実してて、調べながらやりやすい スクリプト化しておくと色々良かった レビューでの説明がある程度楽 レビューでの指摘を柔軟に取り入れられる Conflict 解消、変更への追従が簡単で、他の人の作業をブロックしにくい レビュアーに睨まれたりしない やってみてのまとめ 16