Skip to content

Commit 2606dd0

Browse files
committed
Refactor internals of algo
1 parent 78b75f1 commit 2606dd0

File tree

3 files changed

+108
-105
lines changed

3 files changed

+108
-105
lines changed

lib/index.js

Lines changed: 84 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/**
2-
* @typedef {import('estree-jsx').Node} EstreeNode
3-
* @typedef {import('unist').Node} UnistNode
2+
* @typedef {import('estree-jsx').Node} Nodes
43
*/
54

65
/**
@@ -10,119 +9,112 @@
109
* Leave discouraged fields in the tree (default: `false`).
1110
*/
1211

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+
1428
import {visit} from 'estree-util-visit'
1529
import {positionFromEstree} from 'unist-util-position-from-estree'
1630

17-
const own = {}.hasOwnProperty
31+
/** @type {Options} */
32+
const emptyOptions = {}
1833

1934
/**
2035
* Turn an estree into an esast.
2136
*
22-
* @param {EstreeNode} estree
37+
* @template {Nodes} Kind
38+
* @param {Kind} estree
2339
* estree.
2440
* @param {Options | null | undefined} [options]
2541
* Configuration (optional).
26-
* @returns {UnistNode}
27-
* esast.
42+
* @returns {Kind}
43+
* Clean clone of `estree`.
2844
*/
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')
9778
}
9879
}
9980

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())
10184

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+
}
10791
}
92+
93+
// @ts-expect-error: `position` is not in `Node`, but we add it anyway
94+
// because it’s useful.
95+
node.position = position
10896
}
10997
})
11098

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]
113110
}
114111

115112
/**
116-
* TypeScript helper to check if something is indexable (any object is
117-
* indexable in JavaScript).
118113
*
114+
* @param {string} _
119115
* @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}
125117
*/
126-
function indexable(value) {
127-
assert(value && typeof value === 'object', 'expected object')
118+
function ignoreBigint(_, value) {
119+
return typeof value === 'bigint' ? undefined : value
128120
}

readme.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,15 @@ import {fromEstree} from 'esast-util-from-estree'
7474

7575
// Make acorn support comments and positional info.
7676
const comments = []
77-
const esast = parse(
77+
const estree = parse(
7878
'export function x() { /* Something senseless */ console.log(/(?:)/ + 1n) }',
7979
{sourceType: 'module', locations: true, onComment: comments}
8080
)
81-
esast.comments = comments
81+
estree.comments = comments
8282

83-
console.log(fromEstree(esast))
83+
const esast = fromEstree(estree)
84+
85+
console.log(esast)
8486
```
8587

8688
Yields:
@@ -130,7 +132,7 @@ Turn an estree into an esast.
130132

131133
###### Returns
132134

133-
esast ([`UnistNode`][esast]).
135+
Clean clone of `estree` ([`UnistNode`][esast]).
134136

135137
### `Options`
136138

test.js

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ test('fromEstree', async function (t) {
2020
await t.test('should transform', async function () {
2121
/** @type {EstreeProgram} */
2222
// @ts-expect-error: acorn looks like estree.
23-
const tree = parser.parse('console.log(1)', {
23+
let tree = parser.parse('console.log(1)', {
2424
locations: true,
2525
ecmaVersion: 2021
2626
})
2727

28-
assert.deepEqual(fromEstree(tree), {
28+
tree = fromEstree(tree)
29+
30+
assert.deepEqual(tree, {
2931
type: 'Program',
3032
body: [
3133
{
@@ -90,13 +92,15 @@ test('fromEstree', async function (t) {
9092
await t.test('should transform regexes', async function () {
9193
/** @type {EstreeProgram} */
9294
// @ts-expect-error: acorn looks like estree.
93-
const tree = parser.parse('/(?:)/', {locations: true, ecmaVersion: 2021})
95+
let tree = parser.parse('/(?:)/', {locations: true, ecmaVersion: 2021})
96+
97+
tree = fromEstree(tree)
98+
9499
const statement = tree.body[0]
95100
assert(statement.type === 'ExpressionStatement')
96101

97-
assert.deepEqual(fromEstree(statement.expression), {
102+
assert.deepEqual(statement.expression, {
98103
type: 'Literal',
99-
value: null,
100104
regex: {pattern: '(?:)', flags: ''},
101105
position: {
102106
start: {line: 1, column: 1, offset: 0},
@@ -108,11 +112,14 @@ test('fromEstree', async function (t) {
108112
await t.test('should transform jsx fragments', async function () {
109113
/** @type {EstreeProgram} */
110114
// @ts-expect-error: acorn looks like estree.
111-
const tree = parser.parse('<>b</>', {locations: true, ecmaVersion: 2021})
115+
let tree = parser.parse('<>b</>', {locations: true, ecmaVersion: 2021})
116+
117+
tree = fromEstree(tree)
118+
112119
const statement = tree.body[0]
113120
assert(statement.type === 'ExpressionStatement')
114121

115-
assert.deepEqual(fromEstree(statement.expression), {
122+
assert.deepEqual(statement.expression, {
116123
type: 'JSXFragment',
117124
openingFragment: {
118125
type: 'JSXOpeningFragment',
@@ -160,11 +167,13 @@ test('fromEstree', async function (t) {
160167
while (++index < bigInts.length) {
161168
/** @type {EstreeProgram} */
162169
// @ts-expect-error: acorn looks like estree.
163-
const tree = parser.parse(bigInts[index][0], {
170+
let tree = parser.parse(bigInts[index][0], {
164171
locations: true,
165172
ecmaVersion: 2021
166173
})
167-
fromEstree(tree)
174+
175+
tree = fromEstree(tree)
176+
168177
const statement = tree.body[0]
169178
assert(statement.type === 'ExpressionStatement')
170179
const expression = statement.expression

0 commit comments

Comments
 (0)