本記事はこちらのブログを参考にしています。
翻訳にはアリババクラウドのModelStudio(Qwen)を使用しております。
By Mutao
背景
CPOプロジェクトのコード内のブロックと主要な問題に対処する際、コードガバナンスの効率を高めるツールが必要です。コードを正確に調整するには、自然とAST(抽象構文木)を利用することを考えます。これを使用してコードに必要な調整を行い、対応する問題を解決します。注:この記事のサンプルコードはJavaScriptに基づいています。
ASTとは
ASTとは何ですか?AST(Abstract Syntax Tree)は、ソースコードの構文構造を抽象的に表現したものです。プログラミング言語の構文構造を木構造で表現し、木の各ノードはソースコードの構造を表しています。
画像:インターネットから
ASTは何ができる?
フロントエンド開発者は日常的にJavaScriptコードを書く際に直接ASTを扱うことはあまりありませんが、多くのエンジニアリングツールはそれに関連しています。例えば、Babelによるコード変換、ESLintによる検証、TypeScriptによる型チェック、エディタの構文ハイライトなどです。これらのツールが操作するオブジェクトは、実際にはJavaScriptの抽象構文木です。通常、ASTの実際の使用では以下の3つのステージを経ます:
- Parse: ソースコードをASTに変換します。
- Transform: 各種プラグインを通じてASTを変更します。
-
Generate: コード生成ツールを使用して変更されたASTを再度コードに変換します。
画像:インターネットから
したがって、コードスニペットをASTに変換し、指定された構造的な処理を行い、その後変更されたASTを再度コードに変換することで、コードの修正が達成されます。
ASTの構造
AST Explorerを使用して、コードの抽象構文木構造を確認できます。この記事の例と一貫性を保つために、@babel/parseパーサーを使用することをお勧めします。簡単な例を見てみましょう:javascript
const name = '小明';
変換後のASTツリー構造は以下のようになります:json
{
"type": "Program",
"start": 0,
"end": Ⅶ,
"loc": {
"start": {
"line": 1,
"column": 0,
"index": 0
},
"end": {
"line": 1,
"column": 17,
"index": 17
}
},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 0,
"index": 0
},
"end": {
"line": 1,
"column": 17,
"index": 17
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 6,
"index": 6
},
"end": {
"line": 1,
"column": 17,
"index": 17
}
},
"id": {
"type": "Identifier",
"start": 6,
"end": 10,
"loc": {
"start": {
"line": 1,
"column": 6,
"index": 6
},
"end": {
"line": 1,
"column": 10,
"index": 10
},
"identifierName": "name"
},
"name": "name"
},
"init": {
"type": "StringLiteral",
"start": 13,
"end": 17,
"loc": {
"start": {
"line": 1,
"column": 13,
"index": 13
},
"end": {
"line": 1,
"column": 17,
"index": 17
}
},
"extra": {
"rawValue": "小明",
"raw": "'小明'"
},
"value": "小明"
}
}
],
"kind": "const"
}
],
"directives": []
}
ASTのデータ構造は大きなJSONオブジェクトです。各ノードには、タイプなどの主要な情報が含まれています。
ASTの生成プロセス
JavaScriptの抽象構文木の生成は主にJavaScriptパーサーに依存しており、全体の解析プロセスは2つの段階に分けられます。
字句解析
字句解析、またはトークナイゼーションとは、文字列のシーケンスをトークンのシーケンスに変換するプロセスです。トークンは自然言語の単語と考えることができます。これらは構文解析における最小の実用的な意味を持つ単位であり、構文単位とも呼ばれます。JavaScriptコードの構文単位には以下のようなものがあります:
- キーワード:var, let, const など。
- 識別子:引用符で囲まれていない連続した文字。変数、ifやelseのようなキーワード、trueやfalseのような組み込み定数など。
- 演算子:+, -, *, / など。
- 数値:16進数、10進数、8進数、科学的表現などの構文。
- 文字列:コンピュータにとって、文字列の内容は計算や表示に参加します。
- 空白:連続したスペース、改行、インデント。
- コメント:行コメントやブロックコメントは、分割できない最小の構文単位。
- その他:波括弧、丸括弧、セミコロン、コロン。
トークナイゼーションの例:javascript
// JavaScriptソースコード
const name = '小明';
// トークナイゼーション後の結果
[
{
"value": "const",
"type": "identifier"
},
{
"value": " ",
"type": "whitespace"
},
{
"value": "name",
"type": "identifier"
},
{
"value": " ",
"type": "whitespace"
},
{
"value": "=",
"type": "operator"
},
{
"value": " ",
"type": "whitespace"
},
{
"value": "'小明'",
"type": "string"
},
]
上記のように、トークナイザーはコードスニペットを構文単位に基づいて一連のトークンシーケンスに分解し、ASTへの変換の最初のステップを完了します。これは単純に聞こえますが、トークナイザーを書くには多くのシナリオを考慮し、言語の機能に基づいて様々なケースを処理する必要があります。
構文解析
構文解析は、字句解析の基礎の上にトークンのシーケンスを構文木に結合し、最終的にASTを出力します。以下は、構文パーサーを通過して得られる論理関係を含む抽象構文木です。(主要な部分のみを示します。)
 {
const ast = babelParser.parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
});
return ast;
}
const astResult = babelParse(EXAMPLE_CODE);
console.log(astResult);
/**
{
type: 'Program',
start: 0,
end: ⅹⅹ,
loc: {
start: {
line: 1,
column: 0,
index: 0
},
end: {
line: 1,
column: ⅹⅹ,
index: ⅹⅹ
}
},
sourceType: 'module',
interpreter: null,
body: [
{
type: 'VariableDeclaration',
start: 0,
end: ⅹⅹ,
loc: {
start: {
line: 1,
column: 0,
index: 0
},
end: {
line: 1,
column: ⅹⅹ,
index: ⅹⅹ
}
},
declarations: [
{
type: 'VariableDeclarator',
start: 6,
end: ⅹⅹ,
loc: {
start: {
line: 1,
column: 6,
index: 6
},
end: {
line: 1,
column: ⅹⅹ,
index: ⅹⅹ
}
},
id: {
type: 'Identifier',
start: 6,
end: 10,
loc: {
start: {
line: 1,
column: 6,
index: 6
},
end: {
line: 1,
column: 10,
index: 10
},
identifierName: 'name'
},
name: 'name'
},
init: {
type: 'StringLiteral',
start: 13,
end: ⅹⅹ,
loc: {
start: {
line: 1,
column: 13,
index: 13
},
end: {
line: 1,
column: ⅹⅹ,
index: ⅹⅹ
}
},
value: '小明'
}
}
],
kind: 'const'
}
],
}
*/
3️⃣ @babel/traverse を使用して構文木をトラバースします。traverse
は parse
によって生成された AST を走査し、2つ目の入力パラメータで定義された属性を通じて指定されたノードタイプを反復処理し、handleVariableType
メソッドを使用してノードを変更します。javascript
import traverse from '@babel/traverse';
traverse(astResult, {
VariableDeclaration(path) { // これは type が VariableDeclaration のノードを処理することを示しています。
// ここでノードを処理できます。
handleVariableType(path);
}
});
! 7
コードの AST 構造を詳しく見てみましょう。この構造では、declarations
配列は現在のノードによって定義された変数を表します。この配列の各要素は変数を定義するノードであり、ID プロパティが含まれています。このプロパティには変数名に関する情報が含まれています。ESLint の出力から得た変数名と id.name
を比較します。一致した場合、コードの位置情報も比較します。各ノードには loc
属性があり、現在のノードの位置情報を示しており、これが指定された変数を参照しているかどうかを判断するのに役立ちます。一致が成功したら、node.declarations.splice(index, 1)
を使って現在の変数を削除できます。最後に、node.declarations.length === 0
つまり宣言された変数が残っていない場合、path.remove()
を使ってステートメント全体を削除します。上記の論理に基づき、処理コードを追加します:javascript
// 削除する変数の名前が text で、行番号が line、開始列が startColumn、終了列が endColumn であると仮定
function handleVariableType(path) {
const { node } = path;
node.declarations.forEach((decl, index) => {
if (decl.id.name === 'text') {
if (decl.loc.start.line === line && decl.loc.end.line === line && decl.id.loc.start.column === startColumn && decl.id.loc.end.column === endColumn) {
node.declarations.splice(index, 1);
}
}
});
// 宣言リストが空の場合、宣言ステートメント全体を削除
if (node.declarations.length === 0) {
path.remove();
}
}
この処理ロジックを適用することで、対応する変数宣言ステートメント全体を効果的に削除できます。
特殊なケース
すべての未使用の変数宣言ステートメントを削除できるのでしょうか?次の例を見てみましょう:javascript
const timer = setTimeout(() => {
console.log(a);
}, 1000);
timer
変数は使用されていませんが、単純にステートメント全体を削除するのは適切ではありません。代入式の右側はタイマー関数の返り値であり、後で実行されるロジックを含んでいます。このステートメントを削除するとビジネスロジックに影響を与える可能性がありますので、このようなケースは除外する必要があります。このコードスニペットに対応する AST を見てみましょう。(主要な部分のみ表示)
! 8
AST では、VariableDeclarator
ノード内の init
ノードは、代入式の右側の式を表します。ここではそのタイプは CallExpression
であり、関数の実行結果の返り値を示しています(前の例では StringLiteral
でした)。そのため、handleVariableType
メソッドにチェックを追加する必要があります:init
ノードがこのタイプであれば削除せず、手動で確認して処理するようにします。javascript
// 削除する変数の名前が text で、行番号が line、開始列が startColumn、終了列が endColumn であると仮定
function handleVariableType(path) {
const { node } = path;
node.declarations.forEach((decl, index) => {
if (decl.id.name === 'text') {
if (decl.loc.start.line === line && decl.loc.end.line === line && decl.id.loc.start.column === startColumn && decl.id.loc.end.column === endColumn) {
if (decl.init?.type === 'CallExpression') { // 追加のチェックロジック
// 関数の実行結果の返り値は削除しない;ユーザーが決定する必要がある
} else {
node.declarations.splice(index, 1);
}
}
}
});
// 宣言リストが空の場合、宣言ステートメント全体を削除
if (node.declarations.length === 0) {
path.remove();
}
}
AST を修正した後、@babel/generator を使ってそれを再度コードスニペットに変換し、ソースコードを置き換えます。javascript
import generate from '@babel/generator';
// 修正された AST をコード文字列に戻す
const finalCode =
ロジックチェック: 現在の変数がメソッドの実行結果である場合javascript
const a = setTimeout(() => {
console.log(a);
}, 1000);
const b = arr.map((item) => {
console.log(item);
});
結論
AST(抽象構文木)に徐々に精通していくことで、言語の基礎的な仕組みに対する理解が深まり、コードとの新しい革新的な取り組み方を発見することができます。たとえば、新たな構文糖衣を定義したり、複数の言語間で変換したりするなど、非常に興奮する可能性があります。
免責事項: ここに表明された意見は参考用であり、必ずしもアリババクラウドの公式見解を代表するものではありません。