From adc4852f3aea5bd2847ce9efb53fe43ad030dde6 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:10:51 +0000 Subject: [PATCH] perf(javascript): reuse parsed source across analyses --- src/application/JavaScriptArtifactAnalysis.ts | 33 +++++++++++++------ src/domain/javascriptSemanticAnalysis.ts | 26 ++++++++------- src/domain/javascriptSourceParser.ts | 20 +++++++++++ src/domain/javascriptStaticAnalysis.ts | 31 +++++++++-------- 4 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 src/domain/javascriptSourceParser.ts diff --git a/src/application/JavaScriptArtifactAnalysis.ts b/src/application/JavaScriptArtifactAnalysis.ts index ab12af574..8037e72e5 100644 --- a/src/application/JavaScriptArtifactAnalysis.ts +++ b/src/application/JavaScriptArtifactAnalysis.ts @@ -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, diff --git a/src/domain/javascriptSemanticAnalysis.ts b/src/domain/javascriptSemanticAnalysis.ts index aa71b493c..9cd738329 100644 --- a/src/domain/javascriptSemanticAnalysis.ts +++ b/src/domain/javascriptSemanticAnalysis.ts @@ -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 = {}, +): 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 = {}, ): JavaScriptSemanticIr => { const limits = { ...DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, ...inputLimits }; - let file: ReturnType; - 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); diff --git a/src/domain/javascriptSourceParser.ts b/src/domain/javascriptSourceParser.ts new file mode 100644 index 000000000..673590f09 --- /dev/null +++ b/src/domain/javascriptSourceParser.ts @@ -0,0 +1,20 @@ +import { parse } from "@babel/parser"; + +/** Babel AST produced by REA's inert JavaScript parser boundary. */ +export type ParsedJavaScriptSource = ReturnType; + +/** 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; + } +}; diff --git a/src/domain/javascriptStaticAnalysis.ts b/src/domain/javascriptStaticAnalysis.ts index 1112bfc20..671b264e2 100644 --- a/src/domain/javascriptStaticAnalysis.ts +++ b/src/domain/javascriptStaticAnalysis.ts @@ -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; - 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, + file: ParsedJavaScriptSource, accumulator: AnalysisAccumulator, limits: JavaScriptStaticAnalysisLimits, ): void => { @@ -76,7 +81,7 @@ const traverseStaticSource = ( const finalizeStaticAnalysis = ( source: string, - file: ReturnType, + file: ParsedJavaScriptSource, accumulator: AnalysisAccumulator, limits: JavaScriptStaticAnalysisLimits, ): JavaScriptStaticAnalysis => {