@preventive/compare 1.2.5 → 1.2.6

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 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'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
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; and the blocks checked to take `before` to
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preventive/compare",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "Git-compatible file tree diffs and minified JavaScript comparison",
5
5
  "type": "module",
6
6
  "main": "./compare.js",
@@ -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) => variable.name.length <= RENAMED_MAX_LENGTH && MINIFIED.test(variable.name) && !variable.kept
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/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, and the diff checked to hide no change.
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 = changeSet(renamed, after, options).map(ranges)
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)
@@ -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 { changeSet } from '@preventive/diff'
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 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
- }
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 = linedUp(text(a), text(b), a.length + b.length)
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 }]) {
@@ -174,7 +164,7 @@ function tallied(ka, kb) {
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 = linedUp(ka.key, kb.key, ka.keys.length + kb.keys.length)
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 }]) {
@@ -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'
@@ -36,9 +36,10 @@ function renamedNames(before, after, { extension = 'js', otherExtension = extens
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
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)]
39
+ // by its syntax: a bundle's chunk that one version exports from, the other not,
40
+ // or that neither does, its top level renamed by a minifier as a module's is.
41
+ // Where either version is one, a script's top level is its own too.
42
+ if ([a, b].some((side) => side.module || minifiedTopLevel(side))) [a, b] = [ownTopLevel(a), ownTopLevel(b)]
42
43
  checkAnalysis(before, a)
43
44
  checkAnalysis(after, b)
44
45
  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)