From 8a8efb822677327070ef36fa187f00eafc9d0ef6 Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:36:10 +0000 Subject: [PATCH] fix(javascript): keep unselected sources out of cancellation failures With source_paths, the analysis loop checked cancellation before it recognized an outside-analysis-scope file. Cancellation arriving after the last selected source therefore recorded the next unselected file as a cancelled analysis failure, and the completion loop skipped it as already analyzed. Give out-of-selection classification one owner, used by the loop, the interruption handler and the completion loop. The cancellation is still reported as the interruption. Co-Authored-By: Claude Opus 5.5 --- .../javascript/JavaScriptArtifactAnalysis.ts | 47 ++++++-------- ...vascriptSelectedSourceCancellation.test.ts | 62 +++++++++++++++++++ 2 files changed, 80 insertions(+), 29 deletions(-) create mode 100644 tests/boundary/filesystem/javascript-applications/javascriptSelectedSourceCancellation.test.ts diff --git a/src/application/javascript/JavaScriptArtifactAnalysis.ts b/src/application/javascript/JavaScriptArtifactAnalysis.ts index 81714ada04..b934e4a02a 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysis.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysis.ts @@ -143,25 +143,27 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async ( const selectedFileCount = fileSet.files.length - (fileSet.source_selection?.unselected_files ?? 0); let selectedFileIndex = 0; + // Caller-excluded sources are never analyzed, so neither cancellation nor + // another interruption describes them. + const recordOutsideSelection = (file: JavaScriptArtifactFile): boolean => { + if (file.text.included || file.text.reason !== "outside-analysis-scope") + return false; + state.files.push({ file, javascript: null, semantic: null }); + if (file.kind === "javascript") + projection.recordUnavailableFile( + file, + "Source was not analyzed because it is outside the caller-selected source_paths.", + null, + "incomplete-module", + ); + return true; + }; for (const file of fileSet.files) { try { await setImmediate(); if (signal?.aborted === true) throw new AnalysisCancelledError("analyze_javascript_application"); - if ( - !file.text.included && - file.text.reason === "outside-analysis-scope" - ) { - state.files.push({ file, javascript: null, semantic: null }); - if (file.kind === "javascript") - projection.recordUnavailableFile( - file, - "Source was not analyzed because it is outside the caller-selected source_paths.", - null, - "incomplete-module", - ); - continue; - } + if (recordOutsideSelection(file)) continue; await beforeFile(file, selectedFileIndex++, selectedFileCount); if (file.kind !== "javascript" || !file.text.included) { const steps = analyzeArtifactFileSteps(file, { @@ -252,7 +254,7 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async ( : cause instanceof AnalysisError ? cause : null; - if (interruption !== null) { + if (interruption !== null && !recordOutsideSelection(file)) { state.files.push({ file, javascript: null, @@ -286,21 +288,8 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async ( : null, }; for (const file of fileSet.files) { - if (analyzedPaths.has(file.path)) continue; - if ( - !file.text.included && - file.text.reason === "outside-analysis-scope" - ) { - state.files.push({ file, javascript: null, semantic: null }); - if (file.kind === "javascript") - projection.recordUnavailableFile( - file, - "Source was not analyzed because it is outside the caller-selected source_paths.", - null, - "incomplete-module", - ); + if (analyzedPaths.has(file.path) || recordOutsideSelection(file)) continue; - } state.files.push({ file, javascript: null, diff --git a/tests/boundary/filesystem/javascript-applications/javascriptSelectedSourceCancellation.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptSelectedSourceCancellation.test.ts new file mode 100644 index 0000000000..ac56b3ce90 --- /dev/null +++ b/tests/boundary/filesystem/javascript-applications/javascriptSelectedSourceCancellation.test.ts @@ -0,0 +1,62 @@ +import { expect, it } from "vitest"; + +import { analyzeAndAdoptJavaScriptArtifactFiles } from "../../../../src/application/javascript/JavaScriptArtifactAnalysis.js"; +import { createJavaScriptSemanticGraphProjection } from "../../../../src/application/javascript/JavaScriptSemanticGraphBuilder.js"; +import { readJavaScriptArtifactFiles } from "../../../../src/artifacts/javascript/JavaScriptArtifactFiles.js"; +import { createJavaScriptArtifactReader } from "../../../../src/artifacts/javascript/JavaScriptArtifactReader.js"; +import { createJavaScriptSourceAnalysis } from "../../../../src/artifacts/javascript/JavaScriptSourceAnalysis.js"; +import { JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS } from "../../../../src/domain/javascript/javascriptAnalysisResourceControls.js"; +import { + scanCanonicalArtifactInventory, + withArtifactResourceScope, +} from "../../../fixtures/artifactInventory.js"; +import { writeJavaScriptSourceSelectionFixture } from "../../../fixtures/javascriptSourceSelection.js"; +import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; + +it("keeps unselected sources outside the selection when cancellation follows the last selected source", async () => { + const root = await createTestTempDirectory("rea-js-selected-late-cancel-"); + await writeJavaScriptSourceSelectionFixture(root); + const snapshot = await scanCanonicalArtifactInventory(root, {}); + const reader = createJavaScriptArtifactReader(root, "directory"); + const worker = createJavaScriptSourceAnalysis( + JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS, + 1, + ); + try { + const files = await withArtifactResourceScope((scope) => + readJavaScriptArtifactFiles(reader, snapshot, scope, undefined, [ + "main.mjs", + ]), + ); + const controller = new AbortController(); + // Cancel after the only selected source completes, before the loop + // reaches the first unselected source. + const { analysis, interruption } = + await analyzeAndAdoptJavaScriptArtifactFiles( + files, + { + analyze: async (file, budget, signal) => { + const result = await worker.analyze(file, budget, signal); + controller.abort(); + return result; + }, + }, + createJavaScriptSemanticGraphProjection(), + async () => {}, + controller.signal, + ); + + expect(interruption?._tag).toBe("AnalysisCancelledError"); + const unselected = analysis.files.filter( + ({ file }) => + !file.text.included && file.text.reason === "outside-analysis-scope", + ); + expect(unselected).toHaveLength(5); + expect( + unselected.filter((entry) => entry.analysis_failure !== undefined), + ).toEqual([]); + } finally { + await worker.close(); + await reader.close(); + } +});