mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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<string, string> = {
|
||||
'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');
|
||||
});
|
||||
});
|
||||
@@ -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<string> = new Set(['method', 'function']);
|
||||
const JVM_TYPE_KINDS: ReadonlySet<string> = new Set(['class', 'interface', 'enum', 'struct', 'trait', 'type_alias', 'annotation']);
|
||||
|
||||
const MINIFIED_SCRIPTS = new WeakMap<ResolutionContext, Map<string, boolean>>();
|
||||
|
||||
/** 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<ResolutionContext, Set<string>>();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user