From e7be363f95c673c872f2cf2b432d9dabb2f6e21c Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Wed, 30 Sep 2026 21:09:20 +0000 Subject: [PATCH] fix(js): no call resolves by name into a minified bundle (#2204) A vendored minified script (`*.min.js`, or a script of a few enormous lines - the same detection #2122 uses to rank them last) keeps mangled one-letter names, so a name match from any other file is coincidence: healthchecks' 369 `$(...)` jQuery calls went to a helper inside bootstrap-native.min.js, TestBox's popper.min.js to jquery.min.js internals, flaskbb's docs script to its vendors bundle. Such files' symbols are no longer visible across files by name; calls inside a bundle still resolve. healthchecks -460, flaskbb -48, TestBox -36; bat, lodash, minimal-mistakes, netbox, express, excalidraw unchanged. Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/minified-call-targets.test.ts | 47 +++++++++++++++++++++++++ src/resolution/name-matcher.ts | 20 +++++++++++ 3 files changed, 68 insertions(+) create mode 100644 __tests__/minified-call-targets.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e5872b634..2569e6ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- Calls in your own JavaScript no longer link to a same-named function inside a vendored minified script, like a `*.min.js` or a bundle made of a few enormous lines. Those names are mangled. In healthchecks, every jQuery `$(…)` call had gone to a one-letter helper inside `bootstrap-native.min.js`. - A Swift call on a value built on the spot, like `JSONDecoder().decode(…)` or `Realm().create(…)`, now resolves to that type's method, or to one it inherits. It no longer lands on some other type's method of the same name: Vapor's `JSONDecoder().decode` calls had gone to a request's private container. - A bare Lua call like `ipairs(t)`, `error("…")` or busted's `setup()` now means Lua's own function unless the file defines one. It no longer lands on a table method of the same name: telescope's `ipairs` calls had gone to a linked list's `ipairs`, and kong's `error` calls to its response module. A function's own alias of a host table member, like `local get_query = kong.request.get_query`, is now followed as well. - A C++ call into a macro-opened namespace, like `fmt::format("{}", x)`, now picks the overload its arguments fit. Before, the first overload indexed won: fmt's 1,400 `fmt::format` calls all reached the `text_style` overload in `color.h`. A narrow string literal now fits a narrow format string, a wide `L"…"` fits the wide overload, and the argument count has to fit too. diff --git a/__tests__/minified-call-targets.test.ts b/__tests__/minified-call-targets.test.ts new file mode 100644 index 000000000..b154c0212 --- /dev/null +++ b/__tests__/minified-call-targets.test.ts @@ -0,0 +1,47 @@ +/** + * A vendored minified script's names are mangled, so no other file's call + * means one of them by name: healthchecks' 369 `$(…)` calls — jQuery, loaded + * as a global — went to a one-letter helper inside bootstrap-native.min.js. + * Detected by name (`*.min.js`) or by text (a few enormous lines). + */ +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-minified-')); + const bundle = `!function(){${Array.from({ length: 120 }, (_, i) => `function f${i}(e,t){return e+t+${i}}`).join(';')};function u(e){return f1(e,2)}}();\n`; + const files: Record = { + 'static/js/vendor.min.js': `function $(e){return document.querySelector(e)}function q(e){return $(e)}\n`, + 'static/js/bundle.js': bundle.repeat(4), + 'static/js/app.js': `function init() { + $("#edit-name").click(function () {}); + u("x"); +} +`, + }; + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + cg = await CodeGraph.init(root, { index: true }); +}); + +afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +describe('calls into minified scripts', () => { + it('never resolve by name from another file', () => { + const ids = cg.getNodesInFile('static/js/app.js').map((n) => n.id); + const targets = cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.filePath); + expect(targets).not.toContain('static/js/vendor.min.js'); + expect(targets).not.toContain('static/js/bundle.js'); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 40d45e436..2e62d29be 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -12,6 +12,7 @@ import { blankStringContents, stripCommentsForRegex } from './strip-comments'; import { JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins'; import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-visibility'; import { isTestPath } from '../search/query-utils'; +import { isMinifiedContent } from '../extraction/generated-detection'; /** * Ceiling on how many same-named definitions a FUZZY name-match strategy will * score. A name defined more times than this is "ubiquitous" — a method/symbol @@ -932,6 +933,21 @@ function isLuaLocal(candidate: Node, context: ResolutionContext): boolean { const JVM_CALLABLE_KINDS: ReadonlySet = new Set(['method', 'function']); const JVM_TYPE_KINDS: ReadonlySet = new Set(['class', 'interface', 'enum', 'struct', 'trait', 'type_alias', 'annotation']); +const MINIFIED_SCRIPTS = new WeakMap>(); + +/** A minified / bundled script, by name (`jquery.min.js`) or by its text. */ +function isMinifiedScript(filePath: string, context: ResolutionContext): boolean { + if (!/\.(?:m?js|cjs)$/i.test(filePath)) return false; + let memo = MINIFIED_SCRIPTS.get(context); + if (!memo) MINIFIED_SCRIPTS.set(context, (memo = new Map())); + let hit = memo.get(filePath); + if (hit === undefined) { + hit = /[.-]min\.m?js$/i.test(filePath) || isMinifiedContent(filePath, context.readFile(filePath) ?? ''); + memo.set(filePath, hit); + } + return hit; +} + /** Per context: every package the project's JVM sources declare. */ const JVM_PACKAGES = new WeakMap>(); @@ -1276,6 +1292,9 @@ function rustModuleDir(filePath: string): string { */ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean { if (candidate.filePath === ref.filePath) return true; + // A vendored minified bundle's names are mangled: healthchecks' 369 `$(…)` + // (jQuery, a global) went to a one-letter helper inside bootstrap-native.min.js. + if (isMinifiedScript(candidate.filePath, context)) return false; const lang = candidate.language as string; if (lang === 'c' || lang === 'cpp') { return ( @@ -5095,6 +5114,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { LUA_LOCALS.delete(context); LUA_MEMBERS.delete(context); JVM_PACKAGES.delete(context); + MINIFIED_SCRIPTS.delete(context); PY_LOCAL_BINDS.delete(context); PHP_FILE_SCOPES.delete(context); JAVA_STATIC_IMPORTS.delete(context);