perf(javascript): reuse parsed source across analyses

This commit is contained in:
morluto 2026-07-25 09:10:51 +00:00
parent 674364443d
commit adc4852f3a
4 changed files with 76 additions and 34 deletions

View File

@ -1,16 +1,18 @@
import { createHash } from "node:crypto";
import { analyzeJavaScriptStaticSource } from "../domain/javascriptStaticAnalysis.js";
import { analyzeJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
import { analyzeParsedJavaScriptStaticSource } from "../domain/javascriptStaticAnalysis.js";
import { analyzeParsedJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
import {
DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS,
type JavaScriptSemanticLimits,
} from "../domain/javascriptSemanticIr.js";
import { parseJavaScriptSource } from "../domain/javascriptSourceParser.js";
import type {
JavaScriptSourceRange,
JavaScriptSourcePoint,
JavaScriptStaticAnalysis,
} from "../domain/javascriptStaticAnalysisTypes.js";
import { failedJavaScriptStaticAnalysis } from "../domain/javascriptStaticAnalysisHelpers.js";
import type {
JavaScriptArtifactFile,
JavaScriptArtifactFileSet,
@ -142,13 +144,24 @@ const analyzeArtifactFile = (
state.truncatedScopes += 1;
return;
}
const analysis = analyzeJavaScriptStaticSource(file.text.value, {
maxAstNodes: remainingNodes,
maxFindings: remainingFindings,
maxModules: remainingModules,
deadline: context.deadline,
now: context.now,
});
const parsed = parseJavaScriptSource(file.text.value);
if (parsed === null) {
const analysis = failedJavaScriptStaticAnalysis();
state.files.push({ file, javascript: analysis, semantic: null });
state.parseFailures += 1;
return;
}
const analysis = analyzeParsedJavaScriptStaticSource(
file.text.value,
parsed,
{
maxAstNodes: remainingNodes,
maxFindings: remainingFindings,
maxModules: remainingModules,
deadline: context.deadline,
now: context.now,
},
);
const staticFindings = findingCount(analysis);
const semanticFindingBudget = Math.max(
0,
@ -164,7 +177,7 @@ const analyzeArtifactFile = (
};
const semantics =
analysis.parse_status === "complete" || analysis.parse_status === "partial"
? analyzeJavaScriptSemantics(file.text.value, semanticLimits)
? analyzeParsedJavaScriptSemantics(parsed, semanticLimits)
: null;
state.files.push({
file,

View File

@ -1,4 +1,3 @@
import { parse } from "@babel/parser";
import * as t from "@babel/types";
import {
@ -41,6 +40,10 @@ import {
propertyName,
range,
} from "./javascriptStaticAnalysisHelpers.js";
import {
parseJavaScriptSource,
type ParsedJavaScriptSource,
} from "./javascriptSourceParser.js";
interface BindPatternInput {
readonly pattern: t.Node;
@ -68,18 +71,19 @@ interface AddBindingInput {
export const analyzeJavaScriptSemantics = (
source: string,
inputLimits: Partial<JavaScriptSemanticLimits> = {},
): JavaScriptSemanticIr => {
const file = parseJavaScriptSource(source);
return file === null
? failedJavaScriptSemanticIr()
: analyzeParsedJavaScriptSemantics(file, inputLimits);
};
/** Recover semantics from an already parsed JavaScript artifact. */
export const analyzeParsedJavaScriptSemantics = (
file: ParsedJavaScriptSource,
inputLimits: Partial<JavaScriptSemanticLimits> = {},
): JavaScriptSemanticIr => {
const limits = { ...DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, ...inputLimits };
let file: ReturnType<typeof parse>;
try {
file = parse(source, {
sourceType: "unambiguous",
errorRecovery: true,
plugins: ["jsx", "typescript"],
});
} catch {
return failedJavaScriptSemanticIr();
}
const state = createState(file.program, limits);
collectDefinitions(file.program, state);
const references = collectSemanticReferences(file.program, state);

View File

@ -0,0 +1,20 @@
import { parse } from "@babel/parser";
/** Babel AST produced by REA's inert JavaScript parser boundary. */
export type ParsedJavaScriptSource = ReturnType<typeof parse>;
/** Parse JavaScript or TypeScript once without retaining unused comment nodes. */
export const parseJavaScriptSource = (
source: string,
): ParsedJavaScriptSource | null => {
try {
return parse(source, {
sourceType: "unambiguous",
errorRecovery: true,
attachComment: false,
plugins: ["jsx", "typescript"],
});
} catch {
return null;
}
};

View File

@ -1,4 +1,3 @@
import { parse } from "@babel/parser";
import * as t from "@babel/types";
import { inspectElectronStaticNode } from "./electronStaticAnalysis.js";
@ -29,22 +28,28 @@ import type {
JavaScriptStaticAnalysis,
JavaScriptStaticAnalysisLimits,
} from "./javascriptStaticAnalysisTypes.js";
import {
parseJavaScriptSource,
type ParsedJavaScriptSource,
} from "./javascriptSourceParser.js";
/** Parse one bounded JavaScript artifact and recover static structure only. */
export const analyzeJavaScriptStaticSource = (
source: string,
limits: JavaScriptStaticAnalysisLimits,
): JavaScriptStaticAnalysis => {
let file: ReturnType<typeof parse>;
try {
file = parse(source, {
sourceType: "unambiguous",
errorRecovery: true,
plugins: ["jsx", "typescript"],
});
} catch {
return failedJavaScriptStaticAnalysis();
}
const file = parseJavaScriptSource(source);
return file === null
? failedJavaScriptStaticAnalysis()
: analyzeParsedJavaScriptStaticSource(source, file, limits);
};
/** Recover static structure from an already parsed JavaScript artifact. */
export const analyzeParsedJavaScriptStaticSource = (
source: string,
file: ParsedJavaScriptSource,
limits: JavaScriptStaticAnalysisLimits,
): JavaScriptStaticAnalysis => {
const accumulator = createJavaScriptAnalysisAccumulator();
traverseStaticSource(source, file, accumulator, limits);
addSourceMapDirectives(source, accumulator, limits.maxFindings);
@ -53,7 +58,7 @@ export const analyzeJavaScriptStaticSource = (
const traverseStaticSource = (
source: string,
file: ReturnType<typeof parse>,
file: ParsedJavaScriptSource,
accumulator: AnalysisAccumulator,
limits: JavaScriptStaticAnalysisLimits,
): void => {
@ -76,7 +81,7 @@ const traverseStaticSource = (
const finalizeStaticAnalysis = (
source: string,
file: ReturnType<typeof parse>,
file: ParsedJavaScriptSource,
accumulator: AnalysisAccumulator,
limits: JavaScriptStaticAnalysisLimits,
): JavaScriptStaticAnalysis => {