@preventive/compare 1.2.3 → 1.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/names.d.ts +13 -12
- package/package.json +1 -1
- package/src/names/analyze.js +12 -1
- package/src/names/pairing.js +35 -11
- package/src/names/rename.js +6 -2
package/names.d.ts
CHANGED
|
@@ -21,18 +21,19 @@ export type RenameReason = 'unparsed' | 'dynamic' | 'jsx' | 'block-function' | '
|
|
|
21
21
|
// The diff of two JavaScript files minified on both sides with renamed names
|
|
22
22
|
// set aside. The old side is given the new side's names wherever a minifier
|
|
23
23
|
// renamed a variable alike throughout (a declared variable's short ASCII name,
|
|
24
|
-
// not a script's top level
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// `
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// `
|
|
24
|
+
// not a script's top level -- but where the file's other version is a module,
|
|
25
|
+
// as a bundle's chunk is once it exports, the same file loaded alike -- a name
|
|
26
|
+
// exported or imported as itself, or a shorthand property's), checked to bind
|
|
27
|
+
// every name as before by this package's scope analysis and again by oxc's
|
|
28
|
+
// minifier; formatted again where that changes its layout alone and still binds
|
|
29
|
+
// alike; diffed as diff -U0 has it; and the blocks checked to take `before` to
|
|
30
|
+
// the new side (thrown should they not). `reason` says why no names were given;
|
|
31
|
+
// `reads` the properties either text reads names through (`name`, `toString`,
|
|
32
|
+
// `stack`, `callee`, `caller`), sorted, as code reading its names may act
|
|
33
|
+
// otherwise renamed: none where none is renamed, and `name` only where a name
|
|
34
|
+
// renamed is a function's or class's, the one name it reads (`function f`,
|
|
35
|
+
// `f = () => {}`); `marks` the plain diff's blocks that differ in renamed names
|
|
36
|
+
// alone; and `warnings` what a check that should never fail found.
|
|
36
37
|
export function renamedDiff(before: string, after: string, options?: RenameOptions & { whitespace?: Whitespace }): Promise<{
|
|
37
38
|
before: string
|
|
38
39
|
blocks: Pick<Block, 'a0' | 'a1' | 'b0' | 'b1'>[]
|
package/package.json
CHANGED
package/src/names/analyze.js
CHANGED
|
@@ -234,7 +234,7 @@ function resolved(program, s) {
|
|
|
234
234
|
if (s.naming.has(node)) variable.naming = true
|
|
235
235
|
}
|
|
236
236
|
const own = program.sourceType === 'module' || program.sourceType === 'commonjs' || globals.some(([node]) => COMMONJS.has(node.name))
|
|
237
|
-
for (const variable of s.variables) variable.aside =
|
|
237
|
+
for (const variable of s.variables) variable.aside = renamable(variable) && (own || variable.scope !== s.root)
|
|
238
238
|
// A shorthand property's two names are one, at one place.
|
|
239
239
|
const byStart = (x, y) => x[0].start - y[0].start
|
|
240
240
|
occurrences.sort(byStart)
|
|
@@ -248,6 +248,17 @@ function resolved(program, s) {
|
|
|
248
248
|
return { scopes: s.scopes, variables: s.variables, occurrences: deduplicated, globals, names, reads: s.reads, module: program.sourceType === 'module', commonjs: program.sourceType === 'commonjs', own }
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
// A short minified name, declared, and kept by no export, import or shorthand.
|
|
252
|
+
const renamable = (variable) => variable.name.length <= RENAMED_MAX_LENGTH && MINIFIED.test(variable.name) && !variable.kept
|
|
253
|
+
|
|
254
|
+
// A script's analysis with its top level its own, as a module's: renamable there
|
|
255
|
+
// too. Still read as a script, `this` and all, but no global of it set aside.
|
|
256
|
+
export function ownTopLevel(analysis) {
|
|
257
|
+
if (analysis.reason || analysis.own) return analysis
|
|
258
|
+
for (const variable of analysis.variables) variable.aside = renamable(variable)
|
|
259
|
+
return { ...analysis, own: true }
|
|
260
|
+
}
|
|
261
|
+
|
|
251
262
|
export function analyze(text, extension) {
|
|
252
263
|
const program = parsed(text, extension)
|
|
253
264
|
if (!program) return { reason: 'unparsed' }
|
package/src/names/pairing.js
CHANGED
|
@@ -9,7 +9,17 @@ import { RESERVED } from './syntax.js'
|
|
|
9
9
|
const ANCHORED_RUN = 3
|
|
10
10
|
|
|
11
11
|
// Whitespace alone never tells a name apart.
|
|
12
|
-
const changes = (a, b) => changeSet(a, b, { horizon: 0, whitespace: 'all' })
|
|
12
|
+
const changes = (a, b, minimal = false) => changeSet(a, b, { horizon: 0, whitespace: 'all', minimal })
|
|
13
|
+
|
|
14
|
+
// The search's cutoff can face a run with lines alike far from its own: old code
|
|
15
|
+
// that a block of new code now comes before, its lines faced with that block's
|
|
16
|
+
// braces. The shortest diff instead, where its cost, lines by lines changed, allows.
|
|
17
|
+
const MINIMAL_COST = 2e8
|
|
18
|
+
function linedUp(a, b, lines) {
|
|
19
|
+
const quick = changes(a, b)
|
|
20
|
+
const changed = quick.reduce((n, { a0, a1, b0, b1 }) => n + a1 - a0 + b1 - b0, 0)
|
|
21
|
+
return lines * changed > MINIMAL_COST ? quick : changes(a, b, true)
|
|
22
|
+
}
|
|
13
23
|
|
|
14
24
|
// A text's lines with its renamable variables' names set aside (`key`), but
|
|
15
25
|
// those `written` names (a pair found), each line's variables set aside in
|
|
@@ -69,7 +79,7 @@ const reading = (k, from = 0) => (x, y) => {
|
|
|
69
79
|
// The pairs of lines a diff of `a` and `b` keeps, each told whether slack.
|
|
70
80
|
function* kept(a, b, sameA, sameB) {
|
|
71
81
|
const text = (lines) => lines.map((line) => `${line}\n`).join('')
|
|
72
|
-
const blocks =
|
|
82
|
+
const blocks = linedUp(text(a), text(b), a.length + b.length)
|
|
73
83
|
const [looseA, looseB] = slack(a, b, blocks, sameA, sameB)
|
|
74
84
|
let i = 0, j = 0
|
|
75
85
|
for (const { a0, a1, b1 } of [...blocks, { a0: a.length, a1: a.length, b1: b.length }]) {
|
|
@@ -138,20 +148,33 @@ function count(map, from, to) {
|
|
|
138
148
|
map.get(from).set(to, (map.get(from).get(to) ?? 0) + 1)
|
|
139
149
|
}
|
|
140
150
|
|
|
151
|
+
// Code moved: the lines changes took out and those they put in, diffed on their
|
|
152
|
+
// own, a long run kept pairing as code moved out of order with the rest does.
|
|
153
|
+
function moves(ka, kb, blocks, alike) {
|
|
154
|
+
const at = (ends) => blocks.flatMap((block) => Array.from({ length: block[ends[1]] - block[ends[0]] }, (_, k) => block[ends[0]] + k))
|
|
155
|
+
const news = at(['b0', 'b1']), olds = at(['a0', 'a1'])
|
|
156
|
+
const same = (k, lines) => (x, y) => reading(k)(lines[x], lines[y])
|
|
157
|
+
const pairs = [...kept(plain(olds.map((i) => ka.keys[i])), plain(news.map((j) => kb.keys[j])), same(ka, olds), same(kb, news))]
|
|
158
|
+
for (let end = 1, start = 0; start < pairs.length; start = end++) {
|
|
159
|
+
while (end < pairs.length && pairs[end][0] === pairs[end - 1][0] + 1 && pairs[end][1] === pairs[end - 1][1] + 1) end++
|
|
160
|
+
if (end - start >= ANCHORED_RUN) for (const [x, y, loose] of pairs.slice(start, end)) if (!loose) alike(ka.lines[olds[x]], kb.lines[news[y]])
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
141
164
|
// Pairs of lines counted, a long run of unchanged ones apart from the rest, and
|
|
142
|
-
// apart from both the variables a change's tokens pair (`tokened`)
|
|
143
|
-
// reading variables one side has and the other not
|
|
144
|
-
// unlike lengths, lines alike but for a final `;` or
|
|
145
|
-
// others now follow: `let t = ...;` -> `let t = ...,`).
|
|
165
|
+
// apart from both the variables a change's tokens pair (`tokened`) and those of
|
|
166
|
+
// code moved (`moved`). Lines reading variables one side has and the other not
|
|
167
|
+
// say nothing. In a change of unlike lengths, lines alike but for a final `;` or
|
|
168
|
+
// `,` pair (a declaration others now follow: `let t = ...;` -> `let t = ...,`).
|
|
146
169
|
function tallied(ka, kb) {
|
|
147
|
-
const changed = tallies(), tokened = tallies(), unchanged = tallies()
|
|
170
|
+
const changed = tallies(), moved = tallies(), tokened = tallies(), unchanged = tallies()
|
|
148
171
|
const counted = ({ backward, forward }, variable, other) => {
|
|
149
172
|
count(forward, variable, other)
|
|
150
173
|
count(backward, other, variable)
|
|
151
174
|
}
|
|
152
175
|
const alike = (counts, olds, news) => olds.length === news.length && olds.forEach((variable, k) => counted(counts, variable, news[k]))
|
|
153
176
|
const trimmed = (lines, from, to) => plain(lines.slice(from, to).map((line) => line.replace(/[;,]\s*$/u, '')))
|
|
154
|
-
const blocks =
|
|
177
|
+
const blocks = linedUp(ka.key, kb.key, ka.keys.length + kb.keys.length)
|
|
155
178
|
const [looseA, looseB] = slack(plain(ka.keys), plain(kb.keys), blocks, reading(ka), reading(kb))
|
|
156
179
|
let i = 0, j = 0
|
|
157
180
|
for (const block of [...blocks, { a0: ka.lines.length, a1: ka.lines.length, b0: kb.lines.length, b1: kb.lines.length }]) {
|
|
@@ -169,7 +192,8 @@ function tallied(ka, kb) {
|
|
|
169
192
|
i = block.a1
|
|
170
193
|
j = block.b1
|
|
171
194
|
}
|
|
172
|
-
|
|
195
|
+
moves(ka, kb, blocks, (olds, news) => alike(moved, olds, news))
|
|
196
|
+
return { changed, moved, tokened, unchanged }
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
// The most counted of those not yet paired, none where two tie.
|
|
@@ -215,8 +239,8 @@ export function pairing(before, after, a, b) {
|
|
|
215
239
|
for (const [old, now] of passes) {
|
|
216
240
|
// A pass pairs nothing where every variable of a side is paired already.
|
|
217
241
|
if (!a.variables.some((variable) => variable.aside && !found.map.has(variable)) || !b.variables.some((variable) => variable.aside && !found.paired.has(variable))) break
|
|
218
|
-
const { changed, tokened, unchanged } = tallied(keyed(before, a, old), keyed(after, b, now))
|
|
219
|
-
for (const [counts, tier] of [[unchanged, false], [changed, true], [tokened, true]]) {
|
|
242
|
+
const { changed, moved, tokened, unchanged } = tallied(keyed(before, a, old), keyed(after, b, now))
|
|
243
|
+
for (const [counts, tier] of [[unchanged, false], [changed, true], [tokened, true], [moved, true]]) {
|
|
220
244
|
for (const variable of matched(counts, found, (from, to) => !said.has(from) && !said.has(to))) if (tier) weak.add(variable)
|
|
221
245
|
for (const variable of [...counts.forward.keys(), ...counts.backward.keys()]) said.add(variable)
|
|
222
246
|
}
|
package/src/names/rename.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// to read wrongly, and the result checked again by oxc (bindsAlike).
|
|
4
4
|
|
|
5
5
|
import { getOxc } from '#oxc'
|
|
6
|
-
import { analyze } from './analyze.js'
|
|
6
|
+
import { analyze, ownTopLevel } from './analyze.js'
|
|
7
7
|
import { bindsAlike } from './binds.js'
|
|
8
8
|
import { checkAnalysis, checkGiven, checkPairing, checkRenamed } from './check.js'
|
|
9
9
|
import { misread } from './misread.js'
|
|
@@ -33,8 +33,12 @@ function givenNames(a, b, { map, weak }) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function renamedNames(before, after, { extension = 'js', otherExtension = extension }) {
|
|
36
|
-
|
|
36
|
+
let a = analyze(before, extension), b = analyze(after, otherExtension)
|
|
37
37
|
if (a.reason || b.reason) return { reason: a.reason ?? b.reason }
|
|
38
|
+
// A file is a module by how it is loaded, which its versions share, more than
|
|
39
|
+
// by its syntax: a bundle's chunk that one version exports from, the other not.
|
|
40
|
+
// Where its other version is a module, a script's top level is its own too.
|
|
41
|
+
if (a.module || b.module) [a, b] = [ownTopLevel(a), ownTopLevel(b)]
|
|
38
42
|
checkAnalysis(before, a)
|
|
39
43
|
checkAnalysis(after, b)
|
|
40
44
|
const paired = pairing(before, after, a, b)
|