fix(javascript): retain standalone GraphQL source operations

Retain named operations in GraphQL Source bodies with locationOffset metadata.
Use final property ownership and parsed lexical facts to suppress copies only
when a recovered document, its containers and source aliases have proven
private metadata ownership. Preserve independent observations after writes,
exports, opaque uses, classic-script global exposure or direct evaluation.

Share interpretation across application and captured-web analysis. Build
optional reference indexes once per source and checkpoint examined records,
including suppressed findings, to retain cooperative cancellation.

Fixes #3311

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
Ysopwnable
2026-10-15 15:59:42 +08:00
committed by GitHub
parent c1fd496e6a
commit 6688ed6db7
13 changed files with 986 additions and 234 deletions
@@ -5,15 +5,47 @@ import {
analyzeParsedJavaScriptSemanticsSteps,
classifyParsedJavaScriptElectronBindings,
classifyParsedJavaScriptElectronBindingsSteps,
classifyParsedJavaScriptReceivers,
classifyParsedJavaScriptReceiversSteps,
indexJavaScriptBindingsWithReceiversSteps,
} from "./javascriptSemanticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { createJavaScriptGraphqlScan } from "./javascriptGraphqlOperations.js";
import {
completeSemanticSteps,
traverseJavaScriptAst,
traverseJavaScriptAstSteps,
} from "./javascriptSemanticTraversal.js";
it.each([false, true])(
"checkpoints suppressed GraphQL sources, including alias proof (aliases: %s)",
(aliased) => {
const count = 1100;
const source = Array.from({ length: count }, (_, index) => {
const value = `{body:"query Q${index} { id }",locationOffset:{line:1,column:1}}`;
const declaration = aliased ? `const source${index}=${value};` : "";
return `${declaration}const doc${index}={kind:"OperationDefinition",operation:"query",name:{kind:"Name",value:"Q${index}"},loc:{source:${aliased ? `source${index}` : value}}};`;
}).join("\n");
const parsed = parseJavaScriptSource(source, "sources.mjs");
if (parsed === null) throw new Error("Expected parsed source");
const index = completeSemanticSteps(
indexJavaScriptBindingsWithReceiversSteps(parsed.program),
);
const scan = createJavaScriptGraphqlScan(index);
let operations = 0;
traverseJavaScriptAst(parsed.program, {
enter: (node) => {
if (scan.inspect(node) !== undefined) operations += 1;
},
});
expect(operations).toBe(count);
let checkpoints = 0;
for (const finding of scan.finish()) {
expect(finding).toBeUndefined();
checkpoints += 1;
}
expect(checkpoints).toBeGreaterThanOrEqual(count);
},
);
const source = `
import { readFile } from "node:fs";
const shared = { token: "TOKEN" };
@@ -110,15 +142,14 @@ new NativeWindow();
`;
const parsed = parseJavaScriptSource(text, "receivers.js");
if (parsed === null) throw new Error("Expected parsed source");
const openSteps = classifyParsedJavaScriptReceiversSteps(parsed);
const openSteps = indexJavaScriptBindingsWithReceiversSteps(parsed.program);
let openStep = openSteps.next();
expect(openStep.done).toBe(false);
while (openStep.done !== true) openStep = openSteps.next();
expect([...openStep.value.openReceivers]).toEqual([
expect([...openStep.value.receivers.openReceivers]).toEqual([
[text.indexOf('ambient.open("/local")'), "local"],
[text.indexOf('ambient.open("/global")'), "window"],
]);
expect(openStep.value).toEqual(classifyParsedJavaScriptReceivers(parsed));
const electronSteps = classifyParsedJavaScriptElectronBindingsSteps(parsed);
let electronStep = electronSteps.next();