🧊

ESTreeの`Literal`ノードの`raw`プロパティ

ASTを眺めてるとよく目にするやつ。

ことの発端はすこし別の角度ではあるが、結局こういうのは根本から調べるに限るので・・・。

Literalノードのrawプロパティ

一番わかりやすいのは文字列リテラルに対応するノード。

BabelではStringLiteralだが、ESTree本家では、数値も真偽値もまとめてLiteralと名乗ってる。

で、このLiteralの定義はこうなってる。

interface Literal <: Expression {
    type: "Literal";
    value: string | boolean | null | number | RegExp;
}

https://github.com/estree/estree/blob/master/es5.md#literal

なんと、rawなんてない。 というわけで、ESTreeで決められてるわけではないらしい。

Esprimaが発祥

って、Issueに書いてあった。

.raw property of literal nodes, or something similar · Issue #14 · estree/estree https://github.com/estree/estree/issues/14

確かにEsprimaでは定義されてた。

interface Literal {
    type: 'Literal';
    value: boolean | number | string | RegExp | null;
    raw: string;
    regex?: { pattern: string, flags: string };
}

https://docs.esprima.org/en/latest/syntax-tree-format.html?highlight=raw#literal

というわけで、各parserがそれに追従した結果としての今というわけか。

raw vs value

そもそもrawって何やねんvalueと何が違うんやって。

こういうコードをパースしたら、ASTはだいたいこうなる。

// ['&', "&amp;"]

// ...
[
  {
    "type": "Literal",
    "start": 1,
    "end": 4,
    "value": "&",
    "raw": "'&'"
  },
  {
    "type": "Literal",
    "start": 6,
    "end": 13,
    "value": "&amp;",
    "raw": "\"&amp;\""
  }
]
// ...

というように、文字列の場合、クオートの有無が異なるポイントになる。

数値の場合は、たとえば0xFとか書くと、ちゃんと16進数でvalue: 15となりつつ、raw: "0xF"になってくれて、ASTをコードに戻すときに便利というやつ。

JSXにもある

現世のASTはもはやESTreeで完結せず、JSXがそのいい例。

で、JSXのASTにあるJSXTextノードにも、実はrawプロパティがある。

interface JSXText <: Node {
  type: "JSXText";
  value: string;
  raw: string;
}

https://github.com/facebook/jsx/blob/main/AST.md#jsx-text

あとは、JSXAttributeノードのvalueプロパティにも、Literalノードが含まれることがある。

interface JSXAttribute <: Node {
    type: "JSXAttribute";
    name: JSXIdentifier | JSXNamespacedName;
    value: Literal | JSXExpressionContainer | JSXElement | JSXFragment | null;
}

https://github.com/facebook/jsx/blob/main/AST.md#jsx-attributes

というわけで、JSXのASTにおいても、rawプロパティが見つかる場所はある。

それはそれでいいとして、各parserの実装を追ってみると、(String)Literalのそれとはまた少し違った実装になってるのを見つけた。

HTMLエンティティの対応

JSX内に登場する文字列関連のrawプロパティでは、JSX外でのrawプロパティとは違って、HTMLエンティティが解決される。

つまり、ASTとしてパースされると、このようになる。

// <A p="&amp;">&gt;</A>

// ...
{
  "type": "JSXAttribute",
  "start": 3,
  "end": 12,
  "name": {
    "type": "JSXIdentifier",
    "start": 3,
    "end": 4,
    "name": "p"
  },
  "value": {
    "type": "Literal",
    "start": 5,
    "end": 12,
    "value": "&", // 👈🏻
    "raw": "\"&amp;\""
  }
}
// ...
{
  "type": "JSXText",
  "start": 13,
  "end": 17,
  "value": ">", // 👈🏻
  "raw": "&gt;"
}

ちゃんと仕様にも書いてあった。

http://facebook.github.io/jsx/#sec-jsx-JSXString-SV

ただ、どこで対応するかは実装依存らしく、parserはほとんど対応してるけど、TypeScriptはtransform時にやってた。

https://github.com/babel/babel/blob/143064d1b83e56f22db56b2787523a3eb26a5ac6/packages/babel-parser/src/plugins/jsx/index.ts#L239 https://github.com/acornjs/acorn-jsx/blob/8ed96d6ddec2065204ba07d924bb2e7bca539ea6/index.js#L220 https://github.com/microsoft/TypeScript/blob/0aac72020ee8414218273f654eb7ce1dc2dd0d6b/src/compiler/transformers/jsx.ts#L621

う〜ん、AST界隈は奥深いね・・・。