@preventive/compare 1.2.8 → 1.2.10
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/compare.d.ts +11 -5
- package/compare.js +3 -1
- package/package.json +8 -1
- package/src/chunks.js +53 -12
- package/src/layout.js +1 -24
- package/src/names/analyze.js +37 -3
- package/src/names/pairing.js +69 -14
- package/src/renames.js +2 -1
- package/src/root.js +29 -0
- package/view.d.ts +3 -0
- package/view.js +14 -0
package/compare.d.ts
CHANGED
|
@@ -120,11 +120,17 @@ export interface LineOptions {
|
|
|
120
120
|
// lowercase letters alone), as `index-CyWAfh-p.js` and `index-hRvO4yU2.js`
|
|
121
121
|
// are, or `main.3f9a2c1d.chunk.js` and `main.8b7e6a5f.chunk.js`. Minified onto
|
|
122
122
|
// a line, such a chunk scores little as git splits it, so it is scored with
|
|
123
|
-
// its spans ending after `,` `;` `=` `{` `}` too, and taken
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
123
|
+
// its spans ending after `,` `;` `=` `{` `}` too, and taken however it scores
|
|
124
|
+
// where each is the other's only such chunk and their names are one but for a
|
|
125
|
+
// hash as a build writes one -- the last 8 to 32 of those characters after a
|
|
126
|
+
// `-` or `.`, with a letter, and a digit or a capital, no date nor a name's
|
|
127
|
+
// part, nor two of one format, a character of a kind in each place -- as a
|
|
128
|
+
// pair by path is (a build's chunk rewritten: playwright's
|
|
129
|
+
// dashboard `index-*.js`, 31% alike from 1.61.1 to 1.62.0), else at git's
|
|
130
|
+
// stricter score for a basename, halfway from -M's to 100%: the best pairs
|
|
131
|
+
// first, its similarity that score, giving way as a pair by path does but only
|
|
132
|
+
// where git takes the candidate it gives way to, and none past the rename
|
|
133
|
+
// limit, as they are scored. git has no such passes, so false is git's own -M.
|
|
128
134
|
//
|
|
129
135
|
// `renameLimit` is git's diff.renameLimit: when more deleted files times added
|
|
130
136
|
// files are left, in a group, after the renames that need no scoring than its
|
package/compare.js
CHANGED
|
@@ -23,7 +23,9 @@ export { renameParts } from './src/paths.js'
|
|
|
23
23
|
|
|
24
24
|
// Two trees taken from different roots lined up: the root between them and
|
|
25
25
|
// each old path, directory and package directory as the new tree lays it out.
|
|
26
|
-
|
|
26
|
+
// The line and path helpers a view reads alone are in view.js too.
|
|
27
|
+
export { layoutPackageDirs, treeLayout } from './src/layout.js'
|
|
28
|
+
export { layoutDir, rootLabel } from './src/root.js'
|
|
27
29
|
|
|
28
30
|
// Whether a TypeScript text holds types alone, so that Node runs nothing of it,
|
|
29
31
|
// as compareTrees stamps a declaration file's change: proven on Node alone, where
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preventive/compare",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.10",
|
|
4
4
|
"description": "Git-compatible file tree diffs and minified JavaScript comparison",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./compare.js",
|
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"./pretty.js": {
|
|
22
22
|
"types": "./pretty.d.ts",
|
|
23
23
|
"default": "./pretty.js"
|
|
24
|
+
},
|
|
25
|
+
"./view.js": {
|
|
26
|
+
"types": "./view.d.ts",
|
|
27
|
+
"default": "./view.js"
|
|
24
28
|
}
|
|
25
29
|
},
|
|
26
30
|
"imports": {
|
|
@@ -46,6 +50,8 @@
|
|
|
46
50
|
"names.js",
|
|
47
51
|
"pretty.d.ts",
|
|
48
52
|
"pretty.js",
|
|
53
|
+
"view.d.ts",
|
|
54
|
+
"view.js",
|
|
49
55
|
"src/blocks.js",
|
|
50
56
|
"src/changes.js",
|
|
51
57
|
"src/chunks.js",
|
|
@@ -71,6 +77,7 @@
|
|
|
71
77
|
"src/pretty.js",
|
|
72
78
|
"src/quote.js",
|
|
73
79
|
"src/renames.js",
|
|
80
|
+
"src/root.js",
|
|
74
81
|
"src/same-bytes.js",
|
|
75
82
|
"src/same-bytes.node.js",
|
|
76
83
|
"src/script.js",
|
package/src/chunks.js
CHANGED
|
@@ -10,6 +10,42 @@ const HASH = /^[\w-]{6,}$/u
|
|
|
10
10
|
const RANDOM = /[\dA-Z]/u
|
|
11
11
|
const SEPARATORS = '-._'
|
|
12
12
|
|
|
13
|
+
// A name's readings as one a build gave a hash, `[name]-[hash]` or
|
|
14
|
+
// `[name].[contenthash]`, the shortest hash first: `[name, hash, extensions]`,
|
|
15
|
+
// the hash 8 to 32 of base64url's characters after a `-` or `.`, with a
|
|
16
|
+
// letter, and a digit or a capital, and only extensions after it, a `-` in
|
|
17
|
+
// one too (`main.AbCd1234.hot-update.js`). None for a date
|
|
18
|
+
// (`migration-20240101.js`) or a word (`lodash.debounce.js`).
|
|
19
|
+
const EXTENSIONS = /^(?:\.[\w-]+)+$/u, HASHED = /^[\w-]{8,32}$/u
|
|
20
|
+
function readings(name) {
|
|
21
|
+
const found = []
|
|
22
|
+
for (let at = name.length - 1; at > 0; at--) {
|
|
23
|
+
if (name[at] !== '-' && name[at] !== '.') continue
|
|
24
|
+
const end = name.indexOf('.', at + 1), extensions = name.slice(end), hash = name.slice(at + 1, end)
|
|
25
|
+
if (end !== -1 && HASHED.test(hash) && EXTENSIONS.test(extensions) && /[A-Za-z]/u.test(hash) && RANDOM.test(hash)) found.push([name.slice(0, at), hash, extensions])
|
|
26
|
+
}
|
|
27
|
+
return found
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Whether two names are one but for a hash: the shortest reading of one whose
|
|
31
|
+
// hash the other's reading with those extensions does not share, its name
|
|
32
|
+
// theirs. A part both share is an extension's (`index-AbCd1234.production2024.js`);
|
|
33
|
+
// a `-` in a hash is the hash's (`index-CyWAfh-p.js`, `codeMirrorModule--QdMvsKi.css`)
|
|
34
|
+
// and one before eight more the name's (`vendor-react-dom-AbCd1234.js`). Two
|
|
35
|
+
// with a character of a kind in each place, a digit for a digit, a capital for
|
|
36
|
+
// a capital, are of one format, a time's or a version's
|
|
37
|
+
// (`migration-20240101T120000.js`, `migration-2024-Oct-14.js`), as two hashes
|
|
38
|
+
// all but never are.
|
|
39
|
+
const shape = (text) => text.replaceAll(/\d/gu, '0').replaceAll(/[a-z]/gu, 'a').replaceAll(/[A-Z]/gu, 'A')
|
|
40
|
+
function oneButHash(a, b) {
|
|
41
|
+
const others = readings(b)
|
|
42
|
+
for (const [name, hash, extensions] of readings(a)) {
|
|
43
|
+
const other = others.find((reading) => reading[2] === extensions)
|
|
44
|
+
if (other && other[1] !== hash) return other[0] === name && shape(other[1]) !== shape(hash)
|
|
45
|
+
}
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
13
49
|
function nameOf(path) {
|
|
14
50
|
const slash = path.lastIndexOf('/')
|
|
15
51
|
const name = path.slice(slash + 1)
|
|
@@ -32,25 +68,30 @@ function oneChunk(a, b) {
|
|
|
32
68
|
}
|
|
33
69
|
|
|
34
70
|
// Chunks git's scored pass has yet to claim, by directory, extension and
|
|
35
|
-
// name's length, each pair at least `bar
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
71
|
+
// name's length, each pair at least `bar` -- or however it scores, where each
|
|
72
|
+
// is the other's only such chunk and their names are one but for a hash
|
|
73
|
+
// (oneButHash), as a pair by path is: the build's own chunk, rewritten
|
|
74
|
+
// (playwright's dashboard `index-*.js`, 31% alike from 1.61 to 1.62):
|
|
75
|
+
// the best first, as git takes its candidates, but for a pair `outdone` by a
|
|
76
|
+
// file's other candidate, set aside and returned, best first, for git to pair
|
|
77
|
+
// that candidate with another file: it holds its files, so that a later pair
|
|
78
|
+
// of either is set aside behind it.
|
|
39
79
|
export function chunkRenames({ sources, dests, pairs, used, record, punctuatedScoreOf }, { remaining, open }, bar, outdone) {
|
|
40
80
|
const names = (files, indices) => new Map(indices.filter((i) => files[i].type === 'file').map((i) => [i, nameOf(files[i].path)]))
|
|
41
81
|
const destNames = names(dests, open), sourceNames = names(sources, remaining)
|
|
42
82
|
const byKey = Map.groupBy(sourceNames.keys(), (s) => sourceNames.get(s).key)
|
|
43
83
|
const opens = Map.groupBy(destNames.keys(), (d) => destNames.get(d).key)
|
|
44
|
-
const
|
|
84
|
+
const chunks = []
|
|
45
85
|
for (const [key, ds] of opens) {
|
|
46
86
|
const ss = byKey.get(key) ?? []
|
|
47
|
-
for (const d of ds) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
for (const d of ds) for (const s of ss) if (oneChunk(sourceNames.get(s).name, destNames.get(d).name)) chunks.push({ s, d })
|
|
88
|
+
}
|
|
89
|
+
const byDest = Map.groupBy(chunks, ({ d }) => d), bySource = Map.groupBy(chunks, ({ s }) => s)
|
|
90
|
+
const candidates = []
|
|
91
|
+
for (const { s, d } of chunks) {
|
|
92
|
+
const only = bySource.get(s).length === 1 && byDest.get(d).length === 1 && oneButHash(sourceNames.get(s).name, destNames.get(d).name)
|
|
93
|
+
const score = punctuatedScoreOf(s, d, only ? 0 : bar)
|
|
94
|
+
if (only || score >= bar) candidates.push({ s, d, score })
|
|
54
95
|
}
|
|
55
96
|
const aside = [], heldDests = new Set(), heldSources = new Set()
|
|
56
97
|
for (const pair of candidates.sort((x, y) => y.score - x.score)) {
|
package/src/layout.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
// Two trees taken from different roots lined up: the directory one's files sit
|
|
2
2
|
// under in the other (`root`), and each old path as the new tree lays it out.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
if (!root) return path
|
|
6
|
-
if (root.base === '') return `${root.other}${path}`
|
|
7
|
-
return path.startsWith(root.base) ? path.slice(root.base.length) : null
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// A path outside a root as one inside it reaches it.
|
|
11
|
-
const outside = (root, path) => `${'../'.repeat(root.base.split('/').length - 1)}${path}`
|
|
4
|
+
import { layoutDir, outside, throughRoot } from './root.js'
|
|
12
5
|
|
|
13
6
|
// Each old path as the new tree lays it out: through the root where the new
|
|
14
7
|
// tree has it there, else as it is where it has that, else through the root,
|
|
@@ -70,25 +63,9 @@ export function treeLayout(basePaths, otherPaths) {
|
|
|
70
63
|
return { root, paths: rootPaths(base, other, root).paths }
|
|
71
64
|
}
|
|
72
65
|
|
|
73
|
-
export function layoutDir({ root, paths }, dir, files) {
|
|
74
|
-
if (!root || dir === '.') return dir
|
|
75
|
-
for (const file of files) {
|
|
76
|
-
const to = paths.get(`${dir}/${file}`)
|
|
77
|
-
if (to?.endsWith(`/${file}`)) return to.slice(0, -file.length - 1)
|
|
78
|
-
}
|
|
79
|
-
const to = throughRoot(root, `${dir}/`)
|
|
80
|
-
return to === null ? outside(root, dir) : to.slice(0, -1) || '.'
|
|
81
|
-
}
|
|
82
|
-
|
|
83
66
|
export function layoutPackageDirs(baseDirs, otherDirs, layout) {
|
|
84
67
|
if (!baseDirs && !otherDirs) return null
|
|
85
68
|
const files = Map.groupBy([...baseDirs ?? []], ([, dir]) => dir)
|
|
86
69
|
const dirs = new Map([...files].map(([dir, entries]) => [dir, layoutDir(layout, dir, entries.map(([path]) => path.slice(dir.length + 1)))]))
|
|
87
70
|
return new Map([...[...baseDirs ?? []].map(([path, dir]) => [layout.paths.get(path) ?? path, dirs.get(dir)]), ...otherDirs ?? []])
|
|
88
71
|
}
|
|
89
|
-
|
|
90
|
-
export function rootLabel(root, prefix) {
|
|
91
|
-
if (!root) return { prefix, from: null, to: null }
|
|
92
|
-
const kept = prefix.startsWith(root.other) ? prefix : ''
|
|
93
|
-
return { prefix: kept, from: `./${root.base}${kept.slice(root.other.length)}`, to: `./${kept || root.other}` }
|
|
94
|
-
}
|
package/src/names/analyze.js
CHANGED
|
@@ -53,6 +53,7 @@ const fnOf = (where) => (where.fn ? where : fnOf(where.parent))
|
|
|
53
53
|
function join(s, kept, gone) {
|
|
54
54
|
if (kept === gone || !s.variables.includes(gone)) return
|
|
55
55
|
for (const path of gone.through ?? []) (kept.through ??= []).push(path)
|
|
56
|
+
kept.varIn ??= gone.varIn
|
|
56
57
|
for (const at of s.scopes) for (const [name, variable] of at.names) if (variable === gone) at.names.set(name, kept)
|
|
57
58
|
for (const [node, [variable, where]] of s.declarations) if (variable === gone) s.declarations.set(node, [kept, where])
|
|
58
59
|
s.variables.splice(s.variables.indexOf(gone), 1)
|
|
@@ -64,6 +65,7 @@ function declareVariables(node, where, s) {
|
|
|
64
65
|
for (const id of node.declarations.flatMap((declarator) => declared(declarator.id))) {
|
|
65
66
|
const variable = declare(s, node.kind === 'var' ? fnOf(where) : where, id)
|
|
66
67
|
if (node.kind !== 'var') continue
|
|
68
|
+
variable.varIn = fnOf(where)
|
|
67
69
|
const path = []
|
|
68
70
|
for (let at = where; !at.fn; at = at.parent) path.push(at)
|
|
69
71
|
if (path.length > 0) (variable.through ??= []).push(path)
|
|
@@ -111,6 +113,17 @@ function namingNames(node, s) {
|
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
// Names a variable is set by after its declaration: an assignment's, an
|
|
117
|
+
// update's, or a loop's that declares none.
|
|
118
|
+
function writtenNames(node, s) {
|
|
119
|
+
switch (node.type) {
|
|
120
|
+
case 'AssignmentExpression': for (const id of declared(node.left)) s.writing.add(id); break
|
|
121
|
+
case 'UpdateExpression': for (const id of declared(node.argument)) s.writing.add(id); break
|
|
122
|
+
case 'ForInStatement': case 'ForOfStatement': if (node.left.type !== 'VariableDeclaration') for (const id of declared(node.left)) s.writing.add(id); break
|
|
123
|
+
default: break
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
114
127
|
// Names that are no variable's (keys, labels), names kept, and the properties
|
|
115
128
|
// names are read through.
|
|
116
129
|
function otherNames(node, s) {
|
|
@@ -166,7 +179,7 @@ function enterFunction(node, where, s) {
|
|
|
166
179
|
for (const child of [node.superClass, node.body, ...node.decorators ?? []]) if (child) s.work.push([child, named])
|
|
167
180
|
return
|
|
168
181
|
}
|
|
169
|
-
const parameters = scope(s, named, true)
|
|
182
|
+
const parameters = Object.assign(scope(s, named, true), { arrow: node.type === 'ArrowFunctionExpression', parameter: true })
|
|
170
183
|
if (node.body?.type === 'BlockStatement' && saysStrict(node.body.body)) parameters.strict = true
|
|
171
184
|
for (const parameter of node.params) {
|
|
172
185
|
for (const id of declared(parameter)) declare(s, parameters, id)
|
|
@@ -202,7 +215,7 @@ function enter(node, where, s) {
|
|
|
202
215
|
|
|
203
216
|
// A work list rather than a recursion: a minified file's expressions nest deeply.
|
|
204
217
|
function walk(program) {
|
|
205
|
-
const s = { declarations: new Map(), scopes: [], variables: [], keeping: new Set(), others: new Set(), naming: new Set(), reads: new Set(), references: [], work: [] }
|
|
218
|
+
const s = { declarations: new Map(), scopes: [], variables: [], keeping: new Set(), others: new Set(), naming: new Set(), writing: new Set(), reads: new Set(), references: [], work: [] }
|
|
206
219
|
s.root = Object.assign(scope(s, null, true), { strict: program.sourceType === 'module' || saysStrict(program.body) })
|
|
207
220
|
s.work.push([program, s.root])
|
|
208
221
|
while (s.work.length > 0) {
|
|
@@ -212,6 +225,7 @@ function walk(program) {
|
|
|
212
225
|
continue
|
|
213
226
|
}
|
|
214
227
|
namingNames(node, s)
|
|
228
|
+
writtenNames(node, s)
|
|
215
229
|
const reason = visit(node, where, s)
|
|
216
230
|
if (reason) return { reason }
|
|
217
231
|
enter(node, where, s)
|
|
@@ -229,9 +243,29 @@ function resolved(program, s) {
|
|
|
229
243
|
if (variable) occurrences.push([node, variable, where])
|
|
230
244
|
else globals.push([node, where])
|
|
231
245
|
}
|
|
246
|
+
// Set again (`reassigned`): written, or declared more than once, or a
|
|
247
|
+
// parameter of a function that reads `arguments`, which may write it
|
|
248
|
+
// (`arguments[0] = x`, outside strict code) -- an arrow's its function's,
|
|
249
|
+
// and a `var arguments` its function's own `arguments` still, unlike one a
|
|
250
|
+
// `let`, a parameter, a `catch` or an arrow's `var` declares.
|
|
251
|
+
const setting = new Set()
|
|
252
|
+
for (const [node, where] of globals) {
|
|
253
|
+
if (node.name !== 'arguments') continue
|
|
254
|
+
let at = where
|
|
255
|
+
while (at && !(at.parameter && !at.arrow)) at = at.parent
|
|
256
|
+
setting.add(at)
|
|
257
|
+
}
|
|
258
|
+
for (const variable of s.variables) {
|
|
259
|
+
const at = variable.varIn?.parameters ?? variable.varIn
|
|
260
|
+
if (variable.name === 'arguments' && at?.parameter && !at.arrow) setting.add(at)
|
|
261
|
+
}
|
|
262
|
+
for (const at of setting) for (const variable of at?.names.values() ?? []) variable.reassigned = true
|
|
263
|
+
const declaredAlready = new Set()
|
|
232
264
|
for (const [node, variable] of occurrences) {
|
|
233
265
|
if (s.keeping.has(node)) variable.kept = true
|
|
234
266
|
if (s.naming.has(node)) variable.naming = true
|
|
267
|
+
if (s.writing.has(node) || (s.declarations.has(node) && declaredAlready.has(variable))) variable.reassigned = true
|
|
268
|
+
if (s.declarations.has(node)) declaredAlready.add(variable)
|
|
235
269
|
}
|
|
236
270
|
const own = program.sourceType === 'module' || program.sourceType === 'commonjs' || globals.some(([node]) => COMMONJS.has(node.name))
|
|
237
271
|
for (const variable of s.variables) variable.aside = renamable(variable) && (own || variable.scope !== s.root)
|
|
@@ -245,7 +279,7 @@ function resolved(program, s) {
|
|
|
245
279
|
}
|
|
246
280
|
const deduplicated = occurrences.filter((entry, i) => i === 0 || entry[0].start !== occurrences[i - 1][0].start)
|
|
247
281
|
const names = new Set([...s.variables.map(({ name }) => name), ...globals.map(([node]) => node.name)])
|
|
248
|
-
return { scopes: s.scopes, variables: s.variables, occurrences: deduplicated, globals, names, reads: s.reads, module: program.sourceType === 'module', commonjs: program.sourceType === 'commonjs', own }
|
|
282
|
+
return { scopes: s.scopes, variables: s.variables, occurrences: deduplicated, declaring: new Set(s.declarations.keys()), globals, names, reads: s.reads, module: program.sourceType === 'module', commonjs: program.sourceType === 'commonjs', own }
|
|
249
283
|
}
|
|
250
284
|
|
|
251
285
|
const minified = (name) => name.length <= RENAMED_MAX_LENGTH && MINIFIED.test(name)
|
package/src/names/pairing.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// Which old variable became which new one:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Which old variable became which new one: a variable declared on a line each
|
|
2
|
+
// side holds once is the one declared there on the other; each pair of lines,
|
|
3
|
+
// as the diff of their keys (names set aside) pairs them, counts its variables
|
|
4
|
+
// as standing for each other, and two each the other's most counted pair, a tie
|
|
5
|
+
// pairing neither.
|
|
4
6
|
|
|
5
7
|
import { shortestChangeSet } from '../text.js'
|
|
6
8
|
import { RESERVED } from './syntax.js'
|
|
@@ -8,17 +10,22 @@ import { RESERVED } from './syntax.js'
|
|
|
8
10
|
// Unchanged lines between changes that pair as unchanged ones, not as changed.
|
|
9
11
|
const ANCHORED_RUN = 3
|
|
10
12
|
|
|
13
|
+
// Where a name stood in a key with a slot for each.
|
|
14
|
+
const SLOT = '\0'
|
|
15
|
+
|
|
11
16
|
// Whitespace alone never tells a name apart.
|
|
12
17
|
const KEYS = { horizon: 0, whitespace: 'all' }
|
|
13
18
|
|
|
14
19
|
// A text's lines with its renamable variables' names set aside (`key`), but
|
|
15
20
|
// those `written` names (a pair found), each line's variables set aside in
|
|
16
|
-
// order (`lines`),
|
|
17
|
-
|
|
21
|
+
// order (`lines`), where in its key each stood (`places`), and with `slots`,
|
|
22
|
+
// whether it is declared there (`declares`) and the key with a slot where each
|
|
23
|
+
// stood (`slotted`), so that two keys alike stand for names alike.
|
|
24
|
+
function keyed(text, { occurrences, declaring }, written = () => null, slots = false) {
|
|
18
25
|
const starts = [0]
|
|
19
26
|
for (let at = text.indexOf('\n'); at !== -1; at = text.indexOf('\n', at + 1)) starts.push(at + 1)
|
|
20
|
-
const lines = starts.map(() => []), places = starts.map(() => [])
|
|
21
|
-
let at = 0, key = '', line = 0, removed = 0
|
|
27
|
+
const declares = starts.map(() => []), lines = starts.map(() => []), places = starts.map(() => [])
|
|
28
|
+
let at = 0, key = '', line = 0, removed = 0, slotted = ''
|
|
22
29
|
for (const [node, variable] of occurrences) {
|
|
23
30
|
if (!variable.aside) continue
|
|
24
31
|
const name = written(variable)
|
|
@@ -26,17 +33,19 @@ function keyed(text, { occurrences }, written = () => null) {
|
|
|
26
33
|
if (name === null) {
|
|
27
34
|
lines[line].push(variable)
|
|
28
35
|
places[line].push(node.start - starts[line] - removed)
|
|
36
|
+
if (slots) declares[line].push(declaring.has(node))
|
|
29
37
|
}
|
|
30
38
|
key += text.slice(at, node.start) + (name ?? '')
|
|
39
|
+
if (slots) slotted += text.slice(at, node.start) + (name ?? SLOT)
|
|
31
40
|
removed += node.end - node.start - (name?.length ?? 0)
|
|
32
41
|
at = node.end
|
|
33
42
|
}
|
|
34
43
|
key += text.slice(at)
|
|
35
|
-
return { key, keys: key.split('\n'), lines, places }
|
|
44
|
+
return { key, keys: key.split('\n'), lines, places, declares, slotted: slots ? slotted + text.slice(at) : null }
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
// Lines as a diff that sets whitespace aside compares them.
|
|
39
|
-
const SPACE = /[ \t\n\v\f\r]/gu
|
|
48
|
+
const BLANKS = /[ \t\v\f\r]/gu, SPACE = /[ \t\n\v\f\r]/gu
|
|
40
49
|
const plain = (lines) => lines.map((line) => line.replace(SPACE, ''))
|
|
41
50
|
|
|
42
51
|
// The kept lines of each side a run of changes could stand in place of: a run
|
|
@@ -230,21 +239,67 @@ function matched({ backward, forward }, { map, paired, support }, eligible = ()
|
|
|
230
239
|
return chosen
|
|
231
240
|
}
|
|
232
241
|
|
|
242
|
+
// A variable set once is where it is declared, as its uses may come to read
|
|
243
|
+
// another: `function(e) { ...e.data... }` -> `function(e) { const i = f(e);
|
|
244
|
+
// ...i.data... }`, where more lines read the new `i` as the old `e` did than
|
|
245
|
+
// read `e` still. A line each side holds once, names set aside, is the same
|
|
246
|
+
// line, as patience diff has it, wherever a diff puts it: each variable
|
|
247
|
+
// declared there is the one declared in its place on the other side -- where
|
|
248
|
+
// lines counted say so too, as a short line may stand once on each side for
|
|
249
|
+
// code that is not the same, and where nothing else declared once says
|
|
250
|
+
// otherwise. One set again may be two variables a minifier made one, its
|
|
251
|
+
// declaration the first's alone (`var i = /\d/; ... i = q.prototype`): its
|
|
252
|
+
// lines say.
|
|
253
|
+
function declaredOnce(ka, kb, { changed, tokened, unchanged }) {
|
|
254
|
+
const once = (k) => {
|
|
255
|
+
const where = new Map()
|
|
256
|
+
k.slotted.replace(BLANKS, '').split('\n').forEach((key, line) => where.set(key, where.has(key) ? -1 : line))
|
|
257
|
+
return where
|
|
258
|
+
}
|
|
259
|
+
const from = new Map(), news = once(kb), olds = once(ka), to = new Map()
|
|
260
|
+
const add = (map, x, y) => map.set(x, map.has(x) && map.get(x) !== y ? null : y)
|
|
261
|
+
for (const [key, i] of olds) {
|
|
262
|
+
const j = news.get(key)
|
|
263
|
+
if (i === -1 || j === undefined || j === -1) continue
|
|
264
|
+
ka.lines[i].forEach((variable, n) => {
|
|
265
|
+
if (!ka.declares[i][n] || !kb.declares[j][n] || variable.reassigned || kb.lines[j][n].reassigned) return
|
|
266
|
+
add(to, variable, kb.lines[j][n])
|
|
267
|
+
add(from, kb.lines[j][n], variable)
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
// The first tier counting the pair, as were it found there: its count, and whether that tier's pairs are weak.
|
|
271
|
+
const tiers = [unchanged, changed, tokened]
|
|
272
|
+
return [...to].flatMap(([variable, other]) => {
|
|
273
|
+
const tier = tiers.findIndex(({ forward }) => forward.get(variable)?.has(other))
|
|
274
|
+
return other === null || from.get(other) !== variable || tier === -1 ? [] : [[variable, other, tiers[tier].forward.get(variable).get(other), tier > 0]]
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
233
278
|
// Unchanged lines pair first, and code moved counted with them: a function's
|
|
234
279
|
// code moved into a helper reads its variables as the helper's, the function's
|
|
235
280
|
// own lines as its own, and the most lines decide. Changed ones then pair only
|
|
236
281
|
// what those say nothing of, and a change's tokens only what no line says
|
|
237
282
|
// 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
|
|
239
|
-
//
|
|
240
|
-
//
|
|
283
|
+
// are `weak` pairs, the first to give way where names read wrongly; a pair
|
|
284
|
+
// declared once on each side, found before them all, gives way as the tier
|
|
285
|
+
// first counting it would have it. A second pass reads the pairs found as
|
|
286
|
+
// names, so lines alike with every name set aside and diffed against the wrong
|
|
287
|
+
// one line up by those.
|
|
241
288
|
export function pairing(before, after, a, b) {
|
|
242
289
|
const found = { map: new Map(), paired: new Set(), support: new Map() }, said = new Set(), weak = new Set()
|
|
243
290
|
const passes = [[() => null, () => null], [(variable) => found.map.get(variable)?.name ?? null, (variable) => (found.paired.has(variable) ? variable.name : null)]]
|
|
244
|
-
for (const [old, now] of passes) {
|
|
291
|
+
for (const [pass, [old, now]] of passes.entries()) {
|
|
245
292
|
// A pass pairs nothing where every variable of a side is paired already.
|
|
246
293
|
if (!a.variables.some((variable) => variable.aside && !found.map.has(variable)) || !b.variables.some((variable) => variable.aside && !found.paired.has(variable))) break
|
|
247
|
-
|
|
294
|
+
// Lines declaring once, read in the first pass: the second finds few more, at a cost.
|
|
295
|
+
const ka = keyed(before, a, old, pass === 0), kb = keyed(after, b, now, pass === 0), lines = tallied(ka, kb)
|
|
296
|
+
const { changed, tokened, unchanged } = lines
|
|
297
|
+
for (const [variable, other, support, weakly] of pass === 0 ? declaredOnce(ka, kb, lines) : []) {
|
|
298
|
+
found.map.set(variable, other)
|
|
299
|
+
found.paired.add(other)
|
|
300
|
+
found.support.set(variable, support)
|
|
301
|
+
if (weakly) weak.add(variable)
|
|
302
|
+
}
|
|
248
303
|
for (const [counts, tier] of [[unchanged, false], [changed, true], [tokened, true]]) {
|
|
249
304
|
for (const variable of matched(counts, found, (from, to) => !said.has(from) && !said.has(to))) if (tier) weak.add(variable)
|
|
250
305
|
for (const variable of [...counts.forward.keys(), ...counts.backward.keys()]) said.add(variable)
|
package/src/renames.js
CHANGED
|
@@ -193,7 +193,8 @@ const unpaired = ({ sources, dests, pairs, used }) => ({
|
|
|
193
193
|
})
|
|
194
194
|
|
|
195
195
|
// git's stricter score, halfway from -M's to 100%, for a pair that names alone
|
|
196
|
-
// put forward: a basename both sides have once, or a chunk's
|
|
196
|
+
// put forward: a basename both sides have once, or a chunk's of a name either
|
|
197
|
+
// side has more of.
|
|
197
198
|
const stricter = (minimum) => minimum + Math.trunc(0.5 * (MAX_SCORE - minimum))
|
|
198
199
|
|
|
199
200
|
// Each added file's rename by its index in `dests`: { source, score }. A
|
package/src/root.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// A path through the root two trees are lined up by (layout.js treeLayout),
|
|
2
|
+
// and what a view of them shows of it: a directory of the old tree as the new
|
|
3
|
+
// one lays it out, and the root as a heading reads it. Kept apart from what
|
|
4
|
+
// finds the root, so a view that is given its trees lined up loads none of that.
|
|
5
|
+
|
|
6
|
+
export function throughRoot(root, path) {
|
|
7
|
+
if (!root) return path
|
|
8
|
+
if (root.base === '') return `${root.other}${path}`
|
|
9
|
+
return path.startsWith(root.base) ? path.slice(root.base.length) : null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// A path outside a root as one inside it reaches it.
|
|
13
|
+
export const outside = (root, path) => `${'../'.repeat(root.base.split('/').length - 1)}${path}`
|
|
14
|
+
|
|
15
|
+
export function layoutDir({ root, paths }, dir, files) {
|
|
16
|
+
if (!root || dir === '.') return dir
|
|
17
|
+
for (const file of files) {
|
|
18
|
+
const to = paths.get(`${dir}/${file}`)
|
|
19
|
+
if (to?.endsWith(`/${file}`)) return to.slice(0, -file.length - 1)
|
|
20
|
+
}
|
|
21
|
+
const to = throughRoot(root, `${dir}/`)
|
|
22
|
+
return to === null ? outside(root, dir) : to.slice(0, -1) || '.'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function rootLabel(root, prefix) {
|
|
26
|
+
if (!root) return { prefix, from: null, to: null }
|
|
27
|
+
const kept = prefix.startsWith(root.other) ? prefix : ''
|
|
28
|
+
return { prefix: kept, from: `./${root.base}${kept.slice(root.other.length)}`, to: `./${kept || root.other}` }
|
|
29
|
+
}
|
package/view.d.ts
ADDED
package/view.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// What a view of two trees' changes reads them by, given the files they pair:
|
|
2
|
+
// changed lines read word by word, which removed line was edited into which
|
|
3
|
+
// added one, and whether diff -w hides blanks of more than one kind changing;
|
|
4
|
+
// a rename written as `git diff --stat` writes one; and a directory and the
|
|
5
|
+
// root as the new tree lays them out. compare.js gives these too; this loads
|
|
6
|
+
// none of compareTrees, nor what finds the root, so a view whose files are
|
|
7
|
+
// paired elsewhere (a server's) loads none of the pairing. Typed and described
|
|
8
|
+
// in compare.d.ts.
|
|
9
|
+
|
|
10
|
+
export { pairLines } from './src/pairs.js'
|
|
11
|
+
export { wordRanges } from './src/words.js'
|
|
12
|
+
export { hidesMixedWhitespace } from './src/whitespace.js'
|
|
13
|
+
export { renameParts } from './src/paths.js'
|
|
14
|
+
export { layoutDir, rootLabel } from './src/root.js'
|