|
1 | 1 | /**
|
2 |
| - * @typedef {import('estree-jsx').Node} EstreeNode |
3 |
| - * @typedef {import('unist').Node} UnistNode |
| 2 | + * @typedef {import('estree-jsx').Node} Nodes |
4 | 3 | */
|
5 | 4 |
|
6 | 5 | /**
|
|
10 | 9 | * Leave discouraged fields in the tree (default: `false`).
|
11 | 10 | */
|
12 | 11 |
|
13 |
| -import {ok as assert} from 'devlop' |
| 12 | +/** |
| 13 | + * @template T |
| 14 | + * @template U |
| 15 | + * @typedef {{[K in keyof T]: T[K] extends U ? K : never}[keyof T]} KeysOfType |
| 16 | + */ |
| 17 | + |
| 18 | +/** |
| 19 | + * @template T |
| 20 | + * @typedef {Exclude<KeysOfType<T, Exclude<T[keyof T], undefined>>, undefined>} RequiredKeys |
| 21 | + */ |
| 22 | + |
| 23 | +/** |
| 24 | + * @template T |
| 25 | + * @typedef {Exclude<keyof T, RequiredKeys<T>>} OptionalKeys |
| 26 | + */ |
| 27 | + |
14 | 28 | import {visit} from 'estree-util-visit'
|
15 | 29 | import {positionFromEstree} from 'unist-util-position-from-estree'
|
16 | 30 |
|
17 |
| -const own = {}.hasOwnProperty |
| 31 | +/** @type {Options} */ |
| 32 | +const emptyOptions = {} |
18 | 33 |
|
19 | 34 | /**
|
20 | 35 | * Turn an estree into an esast.
|
21 | 36 | *
|
22 |
| - * @param {EstreeNode} estree |
| 37 | + * @template {Nodes} Kind |
| 38 | + * @param {Kind} estree |
23 | 39 | * estree.
|
24 | 40 | * @param {Options | null | undefined} [options]
|
25 | 41 | * Configuration (optional).
|
26 |
| - * @returns {UnistNode} |
27 |
| - * esast. |
| 42 | + * @returns {Kind} |
| 43 | + * Clean clone of `estree`. |
28 | 44 | */
|
29 |
| -export function fromEstree(estree, options = {}) { |
30 |
| - /** @type {UnistNode | undefined} */ |
31 |
| - let tail |
32 |
| - |
33 |
| - visit(estree, { |
34 |
| - // eslint-disable-next-line complexity |
35 |
| - leave(node, field, index, parents) { |
36 |
| - const parent = parents[parents.length - 1] |
37 |
| - /** @type {EstreeNode} */ |
38 |
| - const context = |
39 |
| - field === undefined || index === undefined |
40 |
| - ? parent |
41 |
| - : // @ts-expect-error: indexable. |
42 |
| - parent[field] |
43 |
| - /** @type {number | string | undefined} */ |
44 |
| - const prop = index === undefined ? field : index |
45 |
| - /** @type {UnistNode} */ |
46 |
| - const copy = {} |
47 |
| - /** @type {string} */ |
48 |
| - let key |
49 |
| - |
50 |
| - for (key in node) { |
51 |
| - if ( |
52 |
| - own.call(node, key) && |
53 |
| - ((options && options.dirty) || |
54 |
| - (key !== 'start' && |
55 |
| - key !== 'end' && |
56 |
| - key !== 'loc' && |
57 |
| - key !== 'raw')) |
58 |
| - ) { |
59 |
| - if ( |
60 |
| - node.type === 'JSXOpeningFragment' && |
61 |
| - (key === 'attributes' || key === 'selfClosing') |
62 |
| - ) { |
63 |
| - continue |
64 |
| - } |
65 |
| - |
66 |
| - indexable(node) |
67 |
| - let value = node[key] |
68 |
| - |
69 |
| - // If this is a bigint or regex literal, reset value. |
70 |
| - if ( |
71 |
| - (!options || !options.dirty) && |
72 |
| - node.type === 'Literal' && |
73 |
| - key === 'value' && |
74 |
| - ('bigint' in node || 'regex' in node) |
75 |
| - ) { |
76 |
| - value = null |
77 |
| - } |
78 |
| - // Normalize `.bigint` to use int notation. |
79 |
| - else if ( |
80 |
| - node.type === 'Literal' && |
81 |
| - key === 'bigint' && |
82 |
| - typeof value === 'string' |
83 |
| - ) { |
84 |
| - const match = /0[box]/.exec(value.slice(0, 2).toLowerCase()) |
85 |
| - |
86 |
| - if (match) { |
87 |
| - const code = match[0].charCodeAt(1) |
88 |
| - value = Number.parseInt( |
89 |
| - value.slice(2), |
90 |
| - code === 98 /* `x` */ ? 2 : code === 111 /* `o` */ ? 8 : 16 |
91 |
| - ).toString() |
92 |
| - } |
93 |
| - } |
94 |
| - |
95 |
| - indexable(copy) |
96 |
| - copy[key] = value |
| 45 | +export function fromEstree(estree, options) { |
| 46 | + const settings = options || emptyOptions |
| 47 | + /** @type {Kind} */ |
| 48 | + // Drop the `Node` and such constructors on Acorn nodes. |
| 49 | + const esast = JSON.parse(JSON.stringify(estree, ignoreBigint)) |
| 50 | + |
| 51 | + visit(esast, { |
| 52 | + leave(node) { |
| 53 | + const position = positionFromEstree(node) |
| 54 | + |
| 55 | + if (!settings.dirty) { |
| 56 | + // Acorn specific. |
| 57 | + // @ts-expect-error: acorn adds this. |
| 58 | + if ('end' in node) remove(node, 'end') |
| 59 | + // @ts-expect-error: acorn adds this. |
| 60 | + if ('start' in node) remove(node, 'start') |
| 61 | + if (node.type === 'JSXOpeningFragment') { |
| 62 | + // @ts-expect-error: acorn adds this, but it should not exist. |
| 63 | + if ('attributes' in node) remove(node, 'attributes') |
| 64 | + // @ts-expect-error: acorn adds this, but it should not exist. |
| 65 | + if ('selfClosing' in node) remove(node, 'selfClosing') |
| 66 | + } |
| 67 | + |
| 68 | + // Estree. |
| 69 | + if ('loc' in node) remove(node, 'loc') |
| 70 | + // @ts-expect-error: `JSXText` types are wrong: `raw` is optional. |
| 71 | + if ('raw' in node) remove(node, 'raw') |
| 72 | + |
| 73 | + if (node.type === 'Literal') { |
| 74 | + // These `value`s on bigint/regex literals represent a raw value, |
| 75 | + // which is an antipattern. |
| 76 | + if ('bigint' in node) remove(node, 'value') |
| 77 | + if ('regex' in node) remove(node, 'value') |
97 | 78 | }
|
98 | 79 | }
|
99 | 80 |
|
100 |
| - copy.position = positionFromEstree(node) |
| 81 | + if (node.type === 'Literal' && 'bigint' in node) { |
| 82 | + const bigint = node.bigint |
| 83 | + const match = /0[box]/.exec(bigint.slice(0, 2).toLowerCase()) |
101 | 84 |
|
102 |
| - if (prop === undefined) { |
103 |
| - tail = copy |
104 |
| - } else { |
105 |
| - indexable(context) |
106 |
| - context[prop] = copy |
| 85 | + if (match) { |
| 86 | + const code = match[0].charCodeAt(1) |
| 87 | + const base = |
| 88 | + code === 98 /* `x` */ ? 2 : code === 111 /* `o` */ ? 8 : 16 |
| 89 | + node.bigint = Number.parseInt(bigint.slice(2), base).toString() |
| 90 | + } |
107 | 91 | }
|
| 92 | + |
| 93 | + // @ts-expect-error: `position` is not in `Node`, but we add it anyway |
| 94 | + // because it’s useful. |
| 95 | + node.position = position |
108 | 96 | }
|
109 | 97 | })
|
110 | 98 |
|
111 |
| - assert(tail, 'expected a node') |
112 |
| - return tail |
| 99 | + return esast |
| 100 | +} |
| 101 | + |
| 102 | +/** |
| 103 | + * @template {Nodes} Kind |
| 104 | + * @param {Kind} value |
| 105 | + * @param {OptionalKeys<Kind>} key |
| 106 | + * @returns {undefined} |
| 107 | + */ |
| 108 | +function remove(value, key) { |
| 109 | + delete value[key] |
113 | 110 | }
|
114 | 111 |
|
115 | 112 | /**
|
116 |
| - * TypeScript helper to check if something is indexable (any object is |
117 |
| - * indexable in JavaScript). |
118 | 113 | *
|
| 114 | + * @param {string} _ |
119 | 115 | * @param {unknown} value
|
120 |
| - * Thing to check. |
121 |
| - * @returns {asserts value is Record<string, unknown>} |
122 |
| - * Nothing. |
123 |
| - * @throws {Error} |
124 |
| - * When `value` is not an object. |
| 116 | + * @returns {unknown} |
125 | 117 | */
|
126 |
| -function indexable(value) { |
127 |
| - assert(value && typeof value === 'object', 'expected object') |
| 118 | +function ignoreBigint(_, value) { |
| 119 | + return typeof value === 'bigint' ? undefined : value |
128 | 120 | }
|
0 commit comments