@preventive/compare 1.2.5 → 1.2.7
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 +7 -4
- package/package.json +1 -1
- package/src/names/analyze.js +11 -1
- package/src/names/check.js +4 -2
- package/src/names/diff.js +4 -4
- package/src/names/pairing.js +39 -34
- package/src/names/rename.js +12 -8
- package/src/text.js +11 -0
package/names.d.ts
CHANGED
|
@@ -21,12 +21,15 @@ 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 -- but where the file
|
|
25
|
-
// as a bundle's chunk is once it exports,
|
|
26
|
-
//
|
|
24
|
+
// not a script's top level -- but where either version of the file is a module,
|
|
25
|
+
// as a bundle's chunk is once it exports, or declares 16 names or more there,
|
|
26
|
+
// every one short, as a minifier renames a script's top level only told that no
|
|
27
|
+
// other script reads it: the same file loaded alike -- a name exported or
|
|
28
|
+
// imported as itself, or a shorthand property's), checked to bind
|
|
27
29
|
// every name as before by this package's scope analysis and again by oxc's
|
|
28
30
|
// minifier; formatted again where that changes its layout alone and still binds
|
|
29
|
-
// alike; diffed as diff -U0 has it
|
|
31
|
+
// alike; diffed as diff -U0 has it, and as --minimal does where that is cheap
|
|
32
|
+
// (lines by lines changed up to 2e8); and the blocks checked to take `before` to
|
|
30
33
|
// the new side (thrown should they not). `reason` says why no names were given;
|
|
31
34
|
// `reads` the properties either text reads names through (`name`, `toString`,
|
|
32
35
|
// `stack`, `callee`, `caller`), sorted, as code reading its names may act
|
package/package.json
CHANGED
package/src/names/analyze.js
CHANGED
|
@@ -248,8 +248,18 @@ 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
|
+
const minified = (name) => name.length <= RENAMED_MAX_LENGTH && MINIFIED.test(name)
|
|
251
252
|
// A short minified name, declared, and kept by no export, import or shorthand.
|
|
252
|
-
const renamable = (variable) =>
|
|
253
|
+
const renamable = (variable) => minified(variable.name) && !variable.kept
|
|
254
|
+
|
|
255
|
+
// A script whose top level a minifier renamed: many names there, every one short.
|
|
256
|
+
// A minifier renames a script's top level only told that no other script reads
|
|
257
|
+
// it, as a bundle's chunk loaded as a module is; globals others read are few.
|
|
258
|
+
const MANY_TOP_LEVEL = 16
|
|
259
|
+
export function minifiedTopLevel(analysis) {
|
|
260
|
+
const top = analysis.variables.filter((variable) => variable.scope.parent === null)
|
|
261
|
+
return top.length >= MANY_TOP_LEVEL && top.every((variable) => minified(variable.name))
|
|
262
|
+
}
|
|
253
263
|
|
|
254
264
|
// A script's analysis with its top level its own, as a module's: renamable there
|
|
255
265
|
// too. Still read as a script, `this` and all, but no global of it set aside.
|
package/src/names/check.js
CHANGED
|
@@ -24,12 +24,14 @@ export function checkAnalysis(text, { scopes, variables, occurrences, globals })
|
|
|
24
24
|
for (const at of scopes) for (const [name, variable] of at.names) holds(known.has(variable) && variable.name === name, `a scope's ${name} no variable of its name`)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// Renamable variables alone paired, each new one once
|
|
28
|
-
|
|
27
|
+
// Renamable variables alone paired, each new one once and by some line, the
|
|
28
|
+
// weak among them.
|
|
29
|
+
export function checkPairing(a, b, { map, support, weak }) {
|
|
29
30
|
const news = new Set(b.variables), taken = new Set()
|
|
30
31
|
for (const [variable, to] of map) {
|
|
31
32
|
holds(variable.aside && to.aside && news.has(to), `${variable.name} paired with ${to.name}, the two not both renamable`)
|
|
32
33
|
holds(!taken.has(to), `two variables paired with ${to.name}`)
|
|
34
|
+
holds(support.get(variable) > 0, `${variable.name} paired with ${to.name} by no line`)
|
|
33
35
|
taken.add(to)
|
|
34
36
|
}
|
|
35
37
|
for (const variable of weak) holds(map.has(variable), `${variable.name} weak, and paired with none`)
|
package/src/names/diff.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// A diff of a file minified on both sides with renamed names set aside: the
|
|
2
2
|
// old side renamed, formatted again where that changes its layout alone and
|
|
3
|
-
// still binds alike, diffed
|
|
3
|
+
// still binds alike, diffed at its shortest where affordable, and the diff
|
|
4
|
+
// checked to hide no change.
|
|
4
5
|
|
|
5
|
-
import { changeSet } from '@preventive/diff'
|
|
6
6
|
import { blocksHold } from '../blocks.js'
|
|
7
7
|
import { markedBlocks } from '../marked.js'
|
|
8
8
|
import { format } from '../pretty.js'
|
|
9
|
-
import { checkLineOptions } from '../text.js'
|
|
9
|
+
import { checkLineOptions, shortestChangeSet } from '../text.js'
|
|
10
10
|
import { bindsAlike } from './binds.js'
|
|
11
11
|
import { renameMap } from './rename.js'
|
|
12
12
|
|
|
@@ -30,7 +30,7 @@ export async function renamedDiffs(before, after, { extension = 'js', otherExten
|
|
|
30
30
|
}
|
|
31
31
|
let unmarked = false
|
|
32
32
|
const diffs = settings.map((options) => {
|
|
33
|
-
const blocks =
|
|
33
|
+
const blocks = shortestChangeSet(renamed, after, options).map(ranges)
|
|
34
34
|
if (!blocksHold(renamed, after, blocks, options)) throw new Error('change blocks that do not take the old side to the new')
|
|
35
35
|
// Read off the renamed text before formatting, line for line the old side's.
|
|
36
36
|
const marks = mapped.reason ? [] : markedBlocks(before, mapped.text, after, options)
|
package/src/names/pairing.js
CHANGED
|
@@ -2,24 +2,14 @@
|
|
|
2
2
|
// their keys (names set aside) pairs them, counts its variables as standing for
|
|
3
3
|
// each other, and two each the other's most counted pair, a tie pairing neither.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { shortestChangeSet } from '../text.js'
|
|
6
6
|
import { RESERVED } from './syntax.js'
|
|
7
7
|
|
|
8
8
|
// Unchanged lines between changes that pair as unchanged ones, not as changed.
|
|
9
9
|
const ANCHORED_RUN = 3
|
|
10
10
|
|
|
11
11
|
// Whitespace alone never tells a name apart.
|
|
12
|
-
const
|
|
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
|
-
}
|
|
12
|
+
const KEYS = { horizon: 0, whitespace: 'all' }
|
|
23
13
|
|
|
24
14
|
// A text's lines with its renamable variables' names set aside (`key`), but
|
|
25
15
|
// those `written` names (a pair found), each line's variables set aside in
|
|
@@ -79,7 +69,7 @@ const reading = (k, from = 0) => (x, y) => {
|
|
|
79
69
|
// The pairs of lines a diff of `a` and `b` keeps, each told whether slack.
|
|
80
70
|
function* kept(a, b, sameA, sameB) {
|
|
81
71
|
const text = (lines) => lines.map((line) => `${line}\n`).join('')
|
|
82
|
-
const blocks =
|
|
72
|
+
const blocks = shortestChangeSet(text(a), text(b), KEYS, a.length + b.length)
|
|
83
73
|
const [looseA, looseB] = slack(a, b, blocks, sameA, sameB)
|
|
84
74
|
let i = 0, j = 0
|
|
85
75
|
for (const { a0, a1, b1 } of [...blocks, { a0: a.length, a1: a.length, b1: b.length }]) {
|
|
@@ -161,20 +151,20 @@ function moves(ka, kb, blocks, alike) {
|
|
|
161
151
|
}
|
|
162
152
|
}
|
|
163
153
|
|
|
164
|
-
// Pairs of lines counted, a long run of unchanged ones apart from the rest,
|
|
165
|
-
// apart from both the variables a change's tokens pair
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
154
|
+
// Pairs of lines counted, a long run of unchanged ones apart from the rest, as
|
|
155
|
+
// is code moved, and apart from both the variables a change's tokens pair
|
|
156
|
+
// (`tokened`). Lines reading variables one side has and the other not say
|
|
157
|
+
// nothing. In a change of unlike lengths, lines alike but for a final `;` or `,`
|
|
158
|
+
// pair (a declaration others now follow: `let t = ...;` -> `let t = ...,`).
|
|
169
159
|
function tallied(ka, kb) {
|
|
170
|
-
const changed = tallies(),
|
|
160
|
+
const changed = tallies(), tokened = tallies(), unchanged = tallies()
|
|
171
161
|
const counted = ({ backward, forward }, variable, other) => {
|
|
172
162
|
count(forward, variable, other)
|
|
173
163
|
count(backward, other, variable)
|
|
174
164
|
}
|
|
175
165
|
const alike = (counts, olds, news) => olds.length === news.length && olds.forEach((variable, k) => counted(counts, variable, news[k]))
|
|
176
166
|
const trimmed = (lines, from, to) => plain(lines.slice(from, to).map((line) => line.replace(/[;,]\s*$/u, '')))
|
|
177
|
-
const blocks =
|
|
167
|
+
const blocks = shortestChangeSet(ka.key, kb.key, KEYS, ka.keys.length + kb.keys.length)
|
|
178
168
|
const [looseA, looseB] = slack(plain(ka.keys), plain(kb.keys), blocks, reading(ka), reading(kb))
|
|
179
169
|
let i = 0, j = 0
|
|
180
170
|
for (const block of [...blocks, { a0: ka.lines.length, a1: ka.lines.length, b0: kb.lines.length, b1: kb.lines.length }]) {
|
|
@@ -192,8 +182,19 @@ function tallied(ka, kb) {
|
|
|
192
182
|
i = block.a1
|
|
193
183
|
j = block.b1
|
|
194
184
|
}
|
|
195
|
-
|
|
196
|
-
|
|
185
|
+
// Code moved counts a pair once a line, so that a moved line reading a variable
|
|
186
|
+
// often outweighs no lines in place reading it once each.
|
|
187
|
+
const once = (olds, news) => {
|
|
188
|
+
if (olds.length !== news.length) return
|
|
189
|
+
const seen = new Map()
|
|
190
|
+
olds.forEach((variable, k) => {
|
|
191
|
+
const others = seen.get(variable) ?? seen.set(variable, new Set()).get(variable)
|
|
192
|
+
if (!others.has(news[k])) counted(unchanged, variable, news[k])
|
|
193
|
+
others.add(news[k])
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
moves(ka, kb, blocks, once)
|
|
197
|
+
return { changed, tokened, unchanged }
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
// The most counted of those not yet paired, none where two tie.
|
|
@@ -209,8 +210,9 @@ function best(counts, taken) {
|
|
|
209
210
|
return found
|
|
210
211
|
}
|
|
211
212
|
|
|
212
|
-
// Pairs each the other's best, into `map`, until none more pair
|
|
213
|
-
|
|
213
|
+
// Pairs each the other's best, into `map`, until none more pair: each pair's
|
|
214
|
+
// count its `support`.
|
|
215
|
+
function matched({ backward, forward }, { map, paired, support }, eligible = () => true) {
|
|
214
216
|
const chosen = []
|
|
215
217
|
for (let more = true; more;) {
|
|
216
218
|
more = false
|
|
@@ -219,6 +221,7 @@ function matched({ backward, forward }, { map, paired }, eligible = () => true)
|
|
|
219
221
|
const to = best(counts, (other) => paired.has(other))
|
|
220
222
|
if (!to || best(backward.get(to), (other) => map.has(other)) !== variable || !eligible(variable, to)) continue
|
|
221
223
|
map.set(variable, to)
|
|
224
|
+
support.set(variable, counts.get(to))
|
|
222
225
|
paired.add(to)
|
|
223
226
|
chosen.push(variable)
|
|
224
227
|
more = true
|
|
@@ -227,23 +230,25 @@ function matched({ backward, forward }, { map, paired }, eligible = () => true)
|
|
|
227
230
|
return chosen
|
|
228
231
|
}
|
|
229
232
|
|
|
230
|
-
// Unchanged lines pair first
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
233
|
+
// Unchanged lines pair first, and code moved counted with them: a function's
|
|
234
|
+
// code moved into a helper reads its variables as the helper's, the function's
|
|
235
|
+
// own lines as its own, and the most lines decide. Changed ones then pair only
|
|
236
|
+
// what those say nothing of, and a change's tokens only what no line says
|
|
237
|
+
// anything of: lines reordered, a change's tokens may line up across them. Both
|
|
238
|
+
// are `weak` pairs, the first to give way where names read wrongly. A second
|
|
239
|
+
// pass reads the pairs found as names, so lines alike with every name set aside
|
|
240
|
+
// and diffed against the wrong one line up by those.
|
|
236
241
|
export function pairing(before, after, a, b) {
|
|
237
|
-
const found = { map: new Map(), paired: new Set() }, said = new Set(), weak = new Set()
|
|
242
|
+
const found = { map: new Map(), paired: new Set(), support: new Map() }, said = new Set(), weak = new Set()
|
|
238
243
|
const passes = [[() => null, () => null], [(variable) => found.map.get(variable)?.name ?? null, (variable) => (found.paired.has(variable) ? variable.name : null)]]
|
|
239
244
|
for (const [old, now] of passes) {
|
|
240
245
|
// A pass pairs nothing where every variable of a side is paired already.
|
|
241
246
|
if (!a.variables.some((variable) => variable.aside && !found.map.has(variable)) || !b.variables.some((variable) => variable.aside && !found.paired.has(variable))) break
|
|
242
|
-
const { changed,
|
|
243
|
-
for (const [counts, tier] of [[unchanged, false], [changed, true], [tokened, true]
|
|
247
|
+
const { changed, tokened, unchanged } = tallied(keyed(before, a, old), keyed(after, b, now))
|
|
248
|
+
for (const [counts, tier] of [[unchanged, false], [changed, true], [tokened, true]]) {
|
|
244
249
|
for (const variable of matched(counts, found, (from, to) => !said.has(from) && !said.has(to))) if (tier) weak.add(variable)
|
|
245
250
|
for (const variable of [...counts.forward.keys(), ...counts.backward.keys()]) said.add(variable)
|
|
246
251
|
}
|
|
247
252
|
}
|
|
248
|
-
return { map: found.map, weak }
|
|
253
|
+
return { map: found.map, support: found.support, weak }
|
|
249
254
|
}
|
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, ownTopLevel } from './analyze.js'
|
|
6
|
+
import { analyze, minifiedTopLevel, 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'
|
|
@@ -11,9 +11,10 @@ import { pairing } from './pairing.js'
|
|
|
11
11
|
import { APART, Broken, holds } from './syntax.js'
|
|
12
12
|
|
|
13
13
|
// Each old variable's name: its counterpart's, else its own, or one apart where
|
|
14
|
-
// the new side has its own. Those misread yield, weak pairs first
|
|
15
|
-
//
|
|
16
|
-
|
|
14
|
+
// the new side has its own. Those misread yield, weak pairs first, else those
|
|
15
|
+
// fewest lines pair: a pair that a few lines alone say gives way to one most
|
|
16
|
+
// do. null where none can.
|
|
17
|
+
function givenNames(a, b, { map, support, weak }) {
|
|
17
18
|
const apart = (name) => {
|
|
18
19
|
let named = name + APART
|
|
19
20
|
for (let n = 2; a.names.has(named) || b.names.has(named); n++) named = `${name}${APART}${n}`
|
|
@@ -27,7 +28,9 @@ function givenNames(a, b, { map, weak }) {
|
|
|
27
28
|
}
|
|
28
29
|
const wrong = [...misread(a, given)]
|
|
29
30
|
if (wrong.length === 0) return given
|
|
30
|
-
const
|
|
31
|
+
const paired = wrong.filter((variable) => map.has(variable))
|
|
32
|
+
const least = paired.reduce((n, variable) => Math.min(n, support.get(variable)), Infinity)
|
|
33
|
+
const yielding = paired.some((variable) => weak.has(variable)) ? paired.filter((variable) => weak.has(variable)) : paired.filter((variable) => support.get(variable) === least)
|
|
31
34
|
if (yielding.filter((variable) => map.delete(variable)).length === 0) return null
|
|
32
35
|
}
|
|
33
36
|
}
|
|
@@ -36,9 +39,10 @@ function renamedNames(before, after, { extension = 'js', otherExtension = extens
|
|
|
36
39
|
let a = analyze(before, extension), b = analyze(after, otherExtension)
|
|
37
40
|
if (a.reason || b.reason) return { reason: a.reason ?? b.reason }
|
|
38
41
|
// 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
|
-
//
|
|
41
|
-
|
|
42
|
+
// by its syntax: a bundle's chunk that one version exports from, the other not,
|
|
43
|
+
// or that neither does, its top level renamed by a minifier as a module's is.
|
|
44
|
+
// Where either version is one, a script's top level is its own too.
|
|
45
|
+
if ([a, b].some((side) => side.module || minifiedTopLevel(side))) [a, b] = [ownTopLevel(a), ownTopLevel(b)]
|
|
42
46
|
checkAnalysis(before, a)
|
|
43
47
|
checkAnalysis(after, b)
|
|
44
48
|
const paired = pairing(before, after, a, b)
|
package/src/text.js
CHANGED
|
@@ -31,6 +31,17 @@ export function checkLineOptions({ horizon = Infinity, whitespace = 'none', igno
|
|
|
31
31
|
return { horizon, whitespace, ignoreCase }
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// The shortest diff where its cost, lines by lines changed, allows, as --minimal
|
|
35
|
+
// has it; past that the search's cutoff, which can face a run with lines alike
|
|
36
|
+
// far from its own: old code that new code now comes before, faced with its braces.
|
|
37
|
+
const SHORTEST_COST = 2e8
|
|
38
|
+
const lineCount = (text) => text.split('\n').length
|
|
39
|
+
export function shortestChangeSet(a, b, options, lines = lineCount(a) + lineCount(b)) {
|
|
40
|
+
const quick = changeSet(a, b, options)
|
|
41
|
+
const changed = quick.reduce((n, { a0, a1, b0, b1 }) => n + a1 - a0 + b1 - b0, 0)
|
|
42
|
+
return lines * changed > SHORTEST_COST ? quick : changeSet(a, b, { ...options, minimal: true })
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
// Binary content has no blocks and no counts, as --numstat prints `-` for it.
|
|
35
46
|
export function contentOf(old, fresh, options) {
|
|
36
47
|
const a = textOf(old), b = textOf(fresh)
|