fix(browser): preserve script-specific parser uncertainty (#2858)
This commit is contained in:
parent
40e94aeb9d
commit
e6c6fdd21a
|
|
@ -8,6 +8,48 @@ import { createWebTextArtifact } from "./webContentArtifact.js";
|
|||
|
||||
const origin = "https://app.example.test";
|
||||
|
||||
it.each([
|
||||
{
|
||||
kind: "recovered",
|
||||
source: "function legacy(a, a) { return a; } fetch('/partial');",
|
||||
parsed: 2,
|
||||
failures: 0,
|
||||
reason: "recovered",
|
||||
endpoints: ["/partial", "/complete"],
|
||||
},
|
||||
{
|
||||
kind: "failed",
|
||||
source: "function broken( {",
|
||||
parsed: 1,
|
||||
failures: 1,
|
||||
reason: "could not be parsed",
|
||||
endpoints: ["/complete"],
|
||||
},
|
||||
])(
|
||||
"attributes $kind parse uncertainty only to its affected source",
|
||||
({ source, parsed, failures, reason, endpoints }) => {
|
||||
const captured = inspection(source);
|
||||
const good = inspection("fetch('/complete');").scripts.items[0];
|
||||
if (good === undefined) throw new Error("Missing valid script fixture");
|
||||
captured.scripts.items.push({ ...good, script_key: "valid-script" });
|
||||
captured.scripts.total += 1;
|
||||
const result = analyzeCapturedWebBundle(captured);
|
||||
expect(result.completeness).toMatchObject({
|
||||
status: "partial",
|
||||
parsed_scripts: parsed,
|
||||
parse_failures: failures,
|
||||
});
|
||||
expect(result.unknowns).toContainEqual({
|
||||
dimension: "javascript_ast",
|
||||
reason: expect.stringContaining(reason),
|
||||
affected_script_keys: [captured.scripts.items[0]?.script_key],
|
||||
});
|
||||
expect(result.observations.endpoints.map(({ value }) => value)).toEqual(
|
||||
endpoints,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
describe("web bundle analyzer", () => {
|
||||
it("extracts graph, route, endpoint, vendor, and WebMCP evidence", () => {
|
||||
const source = `
|
||||
|
|
|
|||
|
|
@ -50,7 +50,8 @@ export interface AnalysisAccumulator {
|
|||
readonly seen: Set<string>;
|
||||
visitedNodes: number;
|
||||
parsedScripts: number;
|
||||
parseFailures: number;
|
||||
readonly failedScripts: string[];
|
||||
readonly recoveredScripts: string[];
|
||||
}
|
||||
|
||||
export const analyzeScript = (
|
||||
|
|
@ -67,10 +68,12 @@ export const analyzeScript = (
|
|||
} catch (cause: unknown) {
|
||||
// Unparseable scripts are counted; the failure needs no extra detail.
|
||||
void cause;
|
||||
accumulator.parseFailures += 1;
|
||||
accumulator.failedScripts.push(script.script_key);
|
||||
return;
|
||||
}
|
||||
accumulator.parsedScripts += 1;
|
||||
if ((file.errors?.length ?? 0) > 0)
|
||||
accumulator.recoveredScripts.push(script.script_key);
|
||||
detectVendorFingerprints(script, accumulator);
|
||||
const openReceiverFacts = classifyParsedJavaScriptOpenReceivers(file);
|
||||
const rpc = createJavaScriptRpcScan(
|
||||
|
|
@ -386,7 +389,8 @@ export const emptyAccumulator = (): AnalysisAccumulator => ({
|
|||
seen: new Set(),
|
||||
visitedNodes: 0,
|
||||
parsedScripts: 0,
|
||||
parseFailures: 0,
|
||||
failedScripts: [],
|
||||
recoveredScripts: [],
|
||||
});
|
||||
|
||||
export const isIncludedScript = (
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ export const buildWebBundleAnalysis = (
|
|||
const sourceMapIncomplete =
|
||||
sourceMaps.status === "partial" || sourceMaps.status === "unavailable";
|
||||
const partial =
|
||||
accumulator.parseFailures > 0 ||
|
||||
accumulator.failedScripts.length > 0 ||
|
||||
accumulator.recoveredScripts.length > 0 ||
|
||||
unavailable.length > 0 ||
|
||||
sourceMapIncomplete ||
|
||||
omittedScripts !== undefined ||
|
||||
|
|
@ -44,9 +45,9 @@ export const buildWebBundleAnalysis = (
|
|||
},
|
||||
inferences: accumulator.inferences,
|
||||
unknowns: buildUnknowns({
|
||||
sourceScripts,
|
||||
unavailable,
|
||||
parseFailures: accumulator.parseFailures,
|
||||
failedScripts: accumulator.failedScripts,
|
||||
recoveredScripts: accumulator.recoveredScripts,
|
||||
sourceMaps,
|
||||
omittedScripts,
|
||||
omittedSourceMaps,
|
||||
|
|
@ -54,7 +55,7 @@ export const buildWebBundleAnalysis = (
|
|||
completeness: buildCompleteness({
|
||||
partial,
|
||||
parsedScripts: accumulator.parsedScripts,
|
||||
parseFailures: accumulator.parseFailures,
|
||||
parseFailures: accumulator.failedScripts.length,
|
||||
visitedNodes: accumulator.visitedNodes,
|
||||
}),
|
||||
limitations: bundleLimitations(),
|
||||
|
|
@ -88,9 +89,9 @@ const chunkNode = (script: IncludedScript) => {
|
|||
};
|
||||
|
||||
interface UnknownsInput {
|
||||
readonly sourceScripts: IncludedScript[];
|
||||
readonly unavailable: string[];
|
||||
readonly parseFailures: number;
|
||||
readonly failedScripts: readonly string[];
|
||||
readonly recoveredScripts: readonly string[];
|
||||
readonly sourceMaps: WebBundleAnalysis["observations"]["source_maps"];
|
||||
readonly omittedScripts: number | null | undefined;
|
||||
readonly omittedSourceMaps: number | null | undefined;
|
||||
|
|
@ -115,15 +116,23 @@ const buildUnknowns = (input: UnknownsInput): WebBundleAnalysis["unknowns"] => [
|
|||
affected_script_keys: input.unavailable,
|
||||
},
|
||||
]),
|
||||
...(input.parseFailures === 0
|
||||
...(input.failedScripts.length === 0
|
||||
? []
|
||||
: [
|
||||
{
|
||||
dimension: "javascript_ast" as const,
|
||||
reason: "One or more source artifacts could not be parsed",
|
||||
affected_script_keys: input.sourceScripts.map(
|
||||
({ script_key }) => script_key,
|
||||
),
|
||||
affected_script_keys: [...input.failedScripts],
|
||||
},
|
||||
]),
|
||||
...(input.recoveredScripts.length === 0
|
||||
? []
|
||||
: [
|
||||
{
|
||||
dimension: "javascript_ast" as const,
|
||||
reason:
|
||||
"The parser recovered from errors in one or more source artifacts; findings may be incomplete",
|
||||
affected_script_keys: [...input.recoveredScripts],
|
||||
},
|
||||
]),
|
||||
...(input.omittedSourceMaps === undefined
|
||||
|
|
|
|||
Loading…
Reference in New Issue