-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Expand file tree
/
Copy pathrefactors.ts
More file actions
177 lines (175 loc) · 6.69 KB
/
Copy pathrefactors.ts
File metadata and controls
177 lines (175 loc) · 6.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import type traverse from '@babel/traverse'
import type { NodePath } from '@babel/traverse'
import * as t from '@babel/types'
import { immutableCollection, mutations } from '#design-diff/mutations'
/** Normalize the equivalent Object.entries record-map idiom without executing its callback. */
export function normalizeRefactors(ast: t.File, visit: typeof traverse): void {
visit(ast, {
CallExpression(path) {
const node = path.node
if (
!t.isMemberExpression(node.callee) ||
node.callee.computed ||
!t.isIdentifier(node.callee.property, { name: 'reduce' }) ||
node.arguments.length !== 2
)
return
const entries = node.callee.object
if (
!t.isCallExpression(entries) ||
!t.isMemberExpression(entries.callee) ||
entries.callee.computed ||
!t.isIdentifier(entries.callee.object, { name: 'Object' }) ||
!t.isIdentifier(entries.callee.property, { name: 'entries' }) ||
entries.arguments.length !== 1 ||
path.scope.getBinding('Object')
)
return
const [callback, initial] = node.arguments
if (
!t.isArrowFunctionExpression(callback) ||
callback.async ||
callback.params.length !== 2 ||
!t.isIdentifier(callback.params[0]) ||
!t.isArrayPattern(callback.params[1]) ||
callback.params[1].elements.length !== 2 ||
!callback.params[1].elements.every(t.isIdentifier) ||
!t.isObjectExpression(initial) ||
initial.properties.length
)
return
const body = callback.body
if (!t.isObjectExpression(body) || body.properties.length !== 2) return
const [spread, property] = body.properties
const accumulator = callback.params[0].name
const key = callback.params[1].elements[0] as t.Identifier
if (
!t.isSpreadElement(spread) ||
!t.isIdentifier(spread.argument, { name: accumulator }) ||
!t.isObjectProperty(property) ||
!property.computed ||
!t.isIdentifier(property.key, { name: key.name })
)
return
let readsAccumulator = false
t.traverseFast(property.value, (node) => {
if (t.isIdentifier(node, { name: accumulator })) readsAccumulator = true
})
if (readsAccumulator || !t.isExpression(property.value)) return
const map = t.callExpression(t.memberExpression(t.cloneNode(entries), t.identifier('map')), [
t.arrowFunctionExpression(
[t.cloneNode(callback.params[1])],
t.arrayExpression([t.cloneNode(key), t.cloneNode(property.value)])
),
])
const replacement = t.callExpression(
t.memberExpression(t.identifier('Object'), t.identifier('fromEntries')),
[map]
)
replacement.loc = node.loc
path.replaceWith(replacement)
path.skip()
},
})
}
/** Fold immutable local literal aliases consistently before resolution budgets are applied. */
export function normalizeLiteralAliases(ast: t.File, visit: typeof traverse): void {
const cached = new WeakMap<t.Node, t.Expression | null>()
const sizes = new WeakMap<t.Node, number>()
/** Bound expanded clone size, including shared literal aliases, rather than depth alone. */
const expandedSize = (node: t.Node): number => {
const cached = sizes.get(node)
if (cached !== undefined) return cached
let size = 1
for (const key of t.VISITOR_KEYS[node.type] ?? []) {
const value = (node as unknown as Record<string, unknown>)[key]
for (const child of Array.isArray(value) ? value : [value]) {
if (child && typeof child === 'object' && 'type' in child)
size += expandedSize(child as t.Node)
if (size > 128) break
}
if (size > 128) break
}
sizes.set(node, size)
return size
}
const literal = (path: NodePath, seen = new Set<t.Node>()): t.Expression | null => {
if (!path.node || seen.has(path.node) || seen.size > 64) return null
if ((path.node.end ?? 0) - (path.node.start ?? 0) > 4096) return null
if (cached.has(path.node)) return cached.get(path.node) ?? null
seen.add(path.node)
let value: t.Expression | null = null
if (
path.isStringLiteral() ||
path.isNumericLiteral() ||
path.isBooleanLiteral() ||
path.isNullLiteral()
)
value = t.cloneNode(path.node)
else if (
path.isTSAsExpression() ||
path.isTSSatisfiesExpression() ||
path.isTSNonNullExpression()
)
value = literal(path.get('expression') as NodePath, seen)
else if (path.isReferencedIdentifier()) {
const binding = path.scope.getBinding(path.node.name)
if (binding?.constant && binding.path.isVariableDeclarator() && !mutations(binding).length) {
value = literal(binding.path.get('init') as NodePath, seen)
if (
value &&
(t.isObjectExpression(value) || t.isArrayExpression(value)) &&
!immutableCollection(binding)
)
value = null
}
} else if (path.isObjectExpression()) {
const properties: t.ObjectProperty[] = []
let valid = true
for (const property of path.get('properties')) {
if (!property.isObjectProperty() || property.node.computed) {
valid = false
break
}
const item = literal(property.get('value') as NodePath, new Set(seen))
const key = property.node.key
if (!item || !(t.isIdentifier(key) || t.isStringLiteral(key) || t.isNumericLiteral(key))) {
valid = false
break
}
properties.push(
t.objectProperty(
t.stringLiteral(t.isIdentifier(key) ? key.name : String(key.value)),
item
)
)
}
if (valid) value = t.objectExpression(properties)
} else if (path.isArrayExpression()) {
const elements = path
.get('elements')
.map((element) => literal(element as NodePath, new Set(seen)))
if (elements.every((item) => item !== null)) value = t.arrayExpression(elements)
}
if (value && expandedSize(value) > 128) value = null
cached.set(path.node, value)
return value
}
visit(ast, {
Expression(path) {
if (path.key === 'id') return
if (path.parentPath.isExportSpecifier()) return
if (path.findParent((parent) => parent.isTSType())) return
if (!(path.isReferencedIdentifier() || path.isObjectExpression() || path.isArrayExpression()))
return
const value = literal(path)
if (!value) return
const replacement = t.cloneNode(value)
replacement.loc = path.node.loc
if (path.parentPath.isObjectProperty() && path.parentPath.node.shorthand)
path.parentPath.node.shorthand = false
path.replaceWith(replacement)
path.skip()
},
})
}