From 69f078e889a1c0fd5a8f7c80bd3f6ec3bbbc0205 Mon Sep 17 00:00:00 2001 From: Joshua Feingold Date: Thu, 21 Aug 2025 16:00:13 -0500 Subject: [PATCH 1/5] NEW @W-17875752@ Implemented failing tests --- package-lock.json | 42 +++++++++---------- packages/ENGINE-TEMPLATE/package.json | 4 +- packages/T-E-M-P-L-A-T-E/package.json | 4 +- packages/code-analyzer-core/package.json | 6 +-- .../code-analyzer-core/src/code-analyzer.ts | 8 +++- .../test/code-analyzer.test.ts | 23 ++++++++-- .../test/rule-selection.test.ts | 21 +++++++--- .../code-analyzer-engine-api/package.json | 4 +- .../code-analyzer-engine-api/src/engines.ts | 12 ++++++ .../code-analyzer-eslint-engine/package.json | 8 ++-- .../code-analyzer-eslint8-engine/package.json | 6 +-- .../code-analyzer-flow-engine/package.json | 6 +-- .../code-analyzer-pmd-engine/package.json | 6 +-- .../code-analyzer-regex-engine/package.json | 6 +-- .../package.json | 6 +-- .../code-analyzer-sfge-engine/package.json | 6 +-- .../package.json | 4 +- 17 files changed, 108 insertions(+), 64 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6bea7717..8b1da131 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8773,10 +8773,10 @@ }, "packages/code-analyzer-core": { "name": "@salesforce/code-analyzer-core", - "version": "0.33.0", + "version": "0.34.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0", "csv-stringify": "^6.6.0", "js-yaml": "^4.1.0", @@ -9033,7 +9033,7 @@ }, "packages/code-analyzer-engine-api": { "name": "@salesforce/code-analyzer-engine-api", - "version": "0.27.0", + "version": "0.28.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { "@types/node": "^20.0.0", @@ -9286,15 +9286,15 @@ }, "packages/code-analyzer-eslint-engine": { "name": "@salesforce/code-analyzer-eslint-engine", - "version": "0.30.0", + "version": "0.31.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { "@eslint/js": "^9.32.0", "@lwc/eslint-plugin-lwc": "^3.2.0", "@lwc/eslint-plugin-lwc-platform": "^6.1.0", "@salesforce-ux/eslint-plugin-slds": "^0.5.0", - "@salesforce/code-analyzer-engine-api": "0.27.0", - "@salesforce/code-analyzer-eslint8-engine": "0.5.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", + "@salesforce/code-analyzer-eslint8-engine": "0.6.0-SNAPSHOT", "@salesforce/eslint-config-lwc": "^4.0.0", "@salesforce/eslint-plugin-lightning": "^2.0.0", "@types/node": "^20.0.0", @@ -9833,7 +9833,7 @@ }, "packages/code-analyzer-eslint8-engine": { "name": "@salesforce/code-analyzer-eslint8-engine", - "version": "0.5.0", + "version": "0.6.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { "@babel/core": "7.27.4", @@ -9841,7 +9841,7 @@ "@eslint/js": "8.57.1", "@lwc/eslint-plugin-lwc": "2.2.0", "@lwc/eslint-plugin-lwc-platform": "5.2.0", - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@salesforce/eslint-config-lwc": "3.7.2", "@salesforce/eslint-plugin-lightning": "1.0.1", "@types/node": "^20.0.0", @@ -10233,10 +10233,10 @@ }, "packages/code-analyzer-flow-engine": { "name": "@salesforce/code-analyzer-flow-engine", - "version": "0.25.0", + "version": "0.26.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0", "semver": "^7.7.2" }, @@ -10487,10 +10487,10 @@ }, "packages/code-analyzer-pmd-engine": { "name": "@salesforce/code-analyzer-pmd-engine", - "version": "0.29.0", + "version": "0.30.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0", "semver": "^7.7.2" }, @@ -10741,10 +10741,10 @@ }, "packages/code-analyzer-regex-engine": { "name": "@salesforce/code-analyzer-regex-engine", - "version": "0.26.0", + "version": "0.27.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0", "isbinaryfile": "^5.0.4", "p-limit": "^3.1.0" @@ -10995,10 +10995,10 @@ }, "packages/code-analyzer-retirejs-engine": { "name": "@salesforce/code-analyzer-retirejs-engine", - "version": "0.25.0", + "version": "0.26.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0", "isbinaryfile": "^5.0.4", "node-stream-zip": "^1.15.0", @@ -11250,10 +11250,10 @@ }, "packages/code-analyzer-sfge-engine": { "name": "@salesforce/code-analyzer-sfge-engine", - "version": "0.11.0", + "version": "0.12.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0", "semver": "^7.7.2" }, @@ -11507,7 +11507,7 @@ "version": "0.1.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0" }, "devDependencies": { @@ -11759,7 +11759,7 @@ "version": "0.1.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0" }, "devDependencies": { @@ -12011,7 +12011,7 @@ "version": "0.1.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0" }, "devDependencies": { diff --git a/packages/ENGINE-TEMPLATE/package.json b/packages/ENGINE-TEMPLATE/package.json index 43206805..12226c1a 100644 --- a/packages/ENGINE-TEMPLATE/package.json +++ b/packages/ENGINE-TEMPLATE/package.json @@ -14,7 +14,7 @@ "types": "dist/index.d.ts", "dependencies": { "@types/node": "^20.0.0", - "@salesforce/code-analyzer-engine-api": "0.27.0" + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT" }, "devDependencies": { "@eslint/js": "^9.32.0", @@ -60,4 +60,4 @@ "!src/index.ts" ] } -} \ No newline at end of file +} diff --git a/packages/T-E-M-P-L-A-T-E/package.json b/packages/T-E-M-P-L-A-T-E/package.json index e41b1894..8a3dd4ea 100644 --- a/packages/T-E-M-P-L-A-T-E/package.json +++ b/packages/T-E-M-P-L-A-T-E/package.json @@ -14,7 +14,7 @@ "types": "dist/index.d.ts", "dependencies": { "@types/node": "^20.0.0", - "@salesforce/code-analyzer-engine-api": "0.27.0" + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT" }, "devDependencies": { "@eslint/js": "^9.32.0", @@ -60,4 +60,4 @@ "!src/index.ts" ] } -} \ No newline at end of file +} diff --git a/packages/code-analyzer-core/package.json b/packages/code-analyzer-core/package.json index f4493704..eb0cf4d8 100644 --- a/packages/code-analyzer-core/package.json +++ b/packages/code-analyzer-core/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-core", "description": "Core Package for the Salesforce Code Analyzer", - "version": "0.33.0", + "version": "0.34.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -13,7 +13,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0", "csv-stringify": "^6.6.0", "js-yaml": "^4.1.0", @@ -69,4 +69,4 @@ "!src/index.ts" ] } -} \ No newline at end of file +} diff --git a/packages/code-analyzer-core/src/code-analyzer.ts b/packages/code-analyzer-core/src/code-analyzer.ts index 6e435704..8342f925 100644 --- a/packages/code-analyzer-core/src/code-analyzer.ts +++ b/packages/code-analyzer-core/src/code-analyzer.ts @@ -333,8 +333,13 @@ export class CodeAnalyzer { if (!this.rulesCache.has(cacheKey)) { this.engineRuleDiscoveryProgressAggregator.reset(this.getEngineNames()); const engApiWorkspace: engApi.Workspace | undefined = workspace ? toEngApiWorkspace(workspace) : undefined; + const rulePromises: Promise[] = this.getEngineNames().map(engineName => - this.getAllRulesFor(engineName, {workspace: engApiWorkspace, logFolder: this.config.getLogFolder()})); + this.getAllRulesFor(engineName, { + workspace: engApiWorkspace, + workingDirectory: '.', + logFolder: this.config.getLogFolder() + })); this.rulesCache.set(cacheKey, (await Promise.all(rulePromises)).flat()); } return this.rulesCache.get(cacheKey)!; @@ -605,6 +610,7 @@ function validateRuleDescriptions(ruleDescriptions: engApi.RuleDescription[], en function extractEngineRunOptions(runOptions: RunOptions, logFolder: string): engApi.RunOptions { return { logFolder: logFolder, + workingDirectory: '.', workspace: toEngApiWorkspace(runOptions.workspace), }; } diff --git a/packages/code-analyzer-core/test/code-analyzer.test.ts b/packages/code-analyzer-core/test/code-analyzer.test.ts index e22df4e3..d06ac011 100644 --- a/packages/code-analyzer-core/test/code-analyzer.test.ts +++ b/packages/code-analyzer-core/test/code-analyzer.test.ts @@ -19,6 +19,7 @@ import { } from "../src"; import * as stubs from "./stubs"; import {getMessage} from "../src/messages"; +import os from "node:os"; import path from "node:path"; import {changeWorkingDirectoryToPackageRoot, FixedUniqueIdGenerator} from "./test-helpers"; import * as engApi from "@salesforce/code-analyzer-engine-api" @@ -186,13 +187,15 @@ describe("Tests for the run method of CodeAnalyzer", () => { let stubEngine1: stubs.StubEngine1; let stubEngine2: stubs.StubEngine2; let selection: RuleSelection; + let fixedClock: FixedClock; const expectedStubEngine1RuleNames: string[] = ['stub1RuleA', 'stub1RuleB', 'stub1RuleC']; const expectedStubEngine2RuleNames: string[] = ['stub2RuleA', 'stub2RuleC']; beforeEach(async () => { sampleTimestamp = new Date(); codeAnalyzer = new CodeAnalyzer(CodeAnalyzerConfig.withDefaults()); - codeAnalyzer._setClock(new FixedClock(sampleTimestamp)); + fixedClock = new FixedClock(sampleTimestamp); + codeAnalyzer._setClock(fixedClock); codeAnalyzer._setUniqueIdGenerator(new FixedUniqueIdGenerator()); sampleRunOptions = {workspace: await codeAnalyzer.createWorkspace([__dirname])}; const stubPlugin: stubs.StubEnginePlugin = new stubs.StubEnginePlugin(); @@ -209,17 +212,26 @@ describe("Tests for the run method of CodeAnalyzer", () => { ]), }); - const expectedEngineRunOptions: engApi.RunOptions = { + const workingDirectoriesRoot: string = `code-analyzer-${fixedClock.formatToDateTimeString()}`; + + const expectedEngineRunOptionsEngine1: engApi.RunOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), + workingDirectory: path.join(os.tmpdir(), workingDirectoriesRoot, 'run', 'stubEngine1'), + workspace: new engApi.Workspace("FixedId", [SAMPLE_WORKSPACE_FOLDER], [ + path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')]) + }; + const expectedEngineRunOptionsEngine2: engApi.RunOptions = { + logFolder: codeAnalyzer.getConfig().getLogFolder(), + workingDirectory: path.join(os.tmpdir(), workingDirectoriesRoot, 'run', 'stubEngine2'), workspace: new engApi.Workspace("FixedId", [SAMPLE_WORKSPACE_FOLDER], [ path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')]) }; expect(stubEngine1.runRulesCallHistory).toHaveLength(1); expect(stubEngine1.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine1RuleNames); - expectEquivalentRunOptions(stubEngine1.runRulesCallHistory[0].runOptions, expectedEngineRunOptions); + expectEquivalentRunOptions(stubEngine1.runRulesCallHistory[0].runOptions, expectedEngineRunOptionsEngine1); expect(stubEngine2.runRulesCallHistory).toHaveLength(1); expect(stubEngine2.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine2RuleNames); - expectEquivalentRunOptions(stubEngine2.runRulesCallHistory[0].runOptions, expectedEngineRunOptions); + expectEquivalentRunOptions(stubEngine2.runRulesCallHistory[0].runOptions, expectedEngineRunOptionsEngine2); }); it("When the workspace provided is one that is not constructed from CodeAnalyzer's createWorkspace method, then it should still work", async () => { @@ -242,8 +254,11 @@ describe("Tests for the run method of CodeAnalyzer", () => { selection = await codeAnalyzer.selectRules(['stubEngine1:Recommended']); await codeAnalyzer.run(selection, sampleRunOptions); + const workingDirectoriesRoot: string = `code-analyzer-${fixedClock.formatToDateTimeString()}`; + const expectedEngineRunOptions: engApi.RunOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), + workingDirectory: path.join(os.tmpdir(), workingDirectoriesRoot, 'run', 'stubEngine1'), workspace: new engApi.Workspace("FixedId", [__dirname]) }; expect(stubEngine1.runRulesCallHistory).toHaveLength(1); diff --git a/packages/code-analyzer-core/test/rule-selection.test.ts b/packages/code-analyzer-core/test/rule-selection.test.ts index bb3c21b1..63a7b751 100644 --- a/packages/code-analyzer-core/test/rule-selection.test.ts +++ b/packages/code-analyzer-core/test/rule-selection.test.ts @@ -16,6 +16,7 @@ import * as engApi from "@salesforce/code-analyzer-engine-api" import {FixedClock} from "@salesforce/code-analyzer-engine-api/utils"; import {RepeatedRuleNameEnginePlugin, StubEnginePlugin} from "./stubs"; import path from "node:path"; +import os from "node:os"; import {changeWorkingDirectoryToPackageRoot, FixedUniqueIdGenerator} from "./test-helpers"; import {getMessage} from "../src/messages"; import * as stubs from "./stubs"; @@ -26,6 +27,7 @@ describe('Tests for selecting rules', () => { let codeAnalyzer: CodeAnalyzer; let plugin: StubEnginePlugin; let sampleTimestamp: Date; + let fixedClock: FixedClock; async function setupCodeAnalyzer(codeAnalyzer: CodeAnalyzer) : Promise { plugin = new StubEnginePlugin(); @@ -37,7 +39,8 @@ describe('Tests for selecting rules', () => { codeAnalyzer = new CodeAnalyzer(CodeAnalyzerConfig.withDefaults()); await setupCodeAnalyzer(codeAnalyzer); sampleTimestamp = new Date(); - codeAnalyzer._setClock(new FixedClock(sampleTimestamp)); + fixedClock = new FixedClock(sampleTimestamp); + codeAnalyzer._setClock(fixedClock); }) it('When no rule selectors are provided then the Recommended tag is used', async () => { @@ -269,14 +272,22 @@ describe('Tests for selecting rules', () => { it('When selectRules is not provided with SelectOptions, then workspace should be undefined for all engines', async () => { await codeAnalyzer.selectRules(['all']); - const expectedDescribeOptions: engApi.DescribeOptions = { + const workingDirectoriesRoot: string = `code-analyzer-${fixedClock.formatToDateTimeString()}`; + + const expectedDescribeOptionsEngine1: engApi.DescribeOptions = { + logFolder: codeAnalyzer.getConfig().getLogFolder(), + workingDirectory: path.join(os.tmpdir(), workingDirectoriesRoot, 'describe', 'stubEngine1'), + workspace: undefined + }; + const expectedDescribeOptionsEngine2: engApi.DescribeOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), + workingDirectory: path.join(os.tmpdir(), workingDirectoriesRoot, 'describe', 'stubEngine2'), workspace: undefined }; const stubEngine1: stubs.StubEngine1 = plugin.getCreatedEngine('stubEngine1') as stubs.StubEngine1; - expect(stubEngine1.describeRulesCallHistory).toEqual([{describeOptions: expectedDescribeOptions}]); + expect(stubEngine1.describeRulesCallHistory).toEqual([{describeOptions: expectedDescribeOptionsEngine1}]); const stubEngine2: stubs.StubEngine2 = plugin.getCreatedEngine('stubEngine2') as stubs.StubEngine2; - expect(stubEngine2.describeRulesCallHistory).toEqual([{describeOptions: expectedDescribeOptions}]); + expect(stubEngine2.describeRulesCallHistory).toEqual([{describeOptions: expectedDescribeOptionsEngine2}]); }); it('When selectRules is provided with SelectOptions, then they are forwarded to the engines', async () => { @@ -285,7 +296,7 @@ describe('Tests for selecting rules', () => { } await codeAnalyzer.selectRules(['all'], selectOptions); - const expectedDescribeOptions: engApi.DescribeOptions = { + const expectedDescribeOptions: Partial = { logFolder: codeAnalyzer.getConfig().getLogFolder(), workspace: new engApi.Workspace('FixedId', [path.resolve('src'), path.resolve('test')]) }; diff --git a/packages/code-analyzer-engine-api/package.json b/packages/code-analyzer-engine-api/package.json index 6c0eba0c..09a03af6 100644 --- a/packages/code-analyzer-engine-api/package.json +++ b/packages/code-analyzer-engine-api/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-engine-api", "description": "Engine API Package for the Salesforce Code Analyzer", - "version": "0.27.0", + "version": "0.28.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -65,4 +65,4 @@ "!src/utils/index.ts" ] } -} \ No newline at end of file +} diff --git a/packages/code-analyzer-engine-api/src/engines.ts b/packages/code-analyzer-engine-api/src/engines.ts index 50727a27..4921cca7 100644 --- a/packages/code-analyzer-engine-api/src/engines.ts +++ b/packages/code-analyzer-engine-api/src/engines.ts @@ -18,6 +18,12 @@ export type DescribeOptions = { */ logFolder: string + /** + * A temporary folder created specifically for use by the current engine. If the engine ever needs to create temporary + * files or folders, that should be done here. + */ + workingDirectory: string + /** * The workspace may or may not be available. If available, then engines should use this workspace object to give a * more accurate list of which of the engine's rules are relevant to the targeted files in the workspace. That is @@ -41,6 +47,12 @@ export type RunOptions = { */ logFolder: string + /** + * A temporary folder created specifically for use by the current engine. If the engine ever needs to create temporary + * files or folders, that should be done here. + */ + workingDirectory: string + /** * The workspace object specifying the files that make up the user's workspace and which files should be targeted. * diff --git a/packages/code-analyzer-eslint-engine/package.json b/packages/code-analyzer-eslint-engine/package.json index 5ee1c52c..e96bc77c 100644 --- a/packages/code-analyzer-eslint-engine/package.json +++ b/packages/code-analyzer-eslint-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-eslint-engine", "description": "Plugin package that adds 'eslint' as an engine into Salesforce Code Analyzer", - "version": "0.30.0", + "version": "0.31.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -17,8 +17,8 @@ "@lwc/eslint-plugin-lwc": "^3.2.0", "@lwc/eslint-plugin-lwc-platform": "^6.1.0", "@salesforce-ux/eslint-plugin-slds": "^0.5.0", - "@salesforce/code-analyzer-engine-api": "0.27.0", - "@salesforce/code-analyzer-eslint8-engine": "0.5.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", + "@salesforce/code-analyzer-eslint8-engine": "0.6.0-SNAPSHOT", "@salesforce/eslint-config-lwc": "^4.0.0", "@salesforce/eslint-plugin-lightning": "^2.0.0", "@types/node": "^20.0.0", @@ -77,4 +77,4 @@ "!src/index.ts" ] } -} \ No newline at end of file +} diff --git a/packages/code-analyzer-eslint8-engine/package.json b/packages/code-analyzer-eslint8-engine/package.json index 9d467d8d..73804633 100644 --- a/packages/code-analyzer-eslint8-engine/package.json +++ b/packages/code-analyzer-eslint8-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-eslint8-engine", "description": "Plugin package that adds 'eslint' (version 8) as an engine into Salesforce Code Analyzer", - "version": "0.5.0", + "version": "0.6.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -18,7 +18,7 @@ "@eslint/js": "8.57.1", "@lwc/eslint-plugin-lwc": "2.2.0", "@lwc/eslint-plugin-lwc-platform": "5.2.0", - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@salesforce/eslint-config-lwc": "3.7.2", "@salesforce/eslint-plugin-lightning": "1.0.1", "@types/node": "^20.0.0", @@ -75,4 +75,4 @@ "!src/index.ts" ] } -} \ No newline at end of file +} diff --git a/packages/code-analyzer-flow-engine/package.json b/packages/code-analyzer-flow-engine/package.json index 7a546391..c912fa73 100644 --- a/packages/code-analyzer-flow-engine/package.json +++ b/packages/code-analyzer-flow-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-flow-engine", "description": "Plugin package that adds 'Flow Scanner' as an engine into Salesforce Code Analyzer", - "version": "0.25.0", + "version": "0.26.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -13,7 +13,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0", "semver": "^7.7.2" }, @@ -63,4 +63,4 @@ "!src/index.ts" ] } -} \ No newline at end of file +} diff --git a/packages/code-analyzer-pmd-engine/package.json b/packages/code-analyzer-pmd-engine/package.json index c24ac80e..6bb28ca3 100644 --- a/packages/code-analyzer-pmd-engine/package.json +++ b/packages/code-analyzer-pmd-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-pmd-engine", "description": "Plugin package that adds 'pmd' and 'cpd' as engines into Salesforce Code Analyzer", - "version": "0.29.0", + "version": "0.30.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -13,7 +13,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0", "semver": "^7.7.2" }, @@ -71,4 +71,4 @@ "!src/index.ts" ] } -} \ No newline at end of file +} diff --git a/packages/code-analyzer-regex-engine/package.json b/packages/code-analyzer-regex-engine/package.json index 92fe4855..e874783f 100644 --- a/packages/code-analyzer-regex-engine/package.json +++ b/packages/code-analyzer-regex-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-regex-engine", "description": "Plugin package that adds 'regex' as an engine into Salesforce Code Analyzer", - "version": "0.26.0", + "version": "0.27.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -13,7 +13,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0", "isbinaryfile": "^5.0.4", "p-limit": "^3.1.0" @@ -62,4 +62,4 @@ "!src/index.ts" ] } -} \ No newline at end of file +} diff --git a/packages/code-analyzer-retirejs-engine/package.json b/packages/code-analyzer-retirejs-engine/package.json index c681883a..ce130089 100644 --- a/packages/code-analyzer-retirejs-engine/package.json +++ b/packages/code-analyzer-retirejs-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-retirejs-engine", "description": "Plugin package that adds 'retire-js' as an engine into Salesforce Code Analyzer", - "version": "0.25.0", + "version": "0.26.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -13,7 +13,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0", "isbinaryfile": "^5.0.4", "node-stream-zip": "^1.15.0", @@ -65,4 +65,4 @@ "!src/index.ts" ] } -} \ No newline at end of file +} diff --git a/packages/code-analyzer-sfge-engine/package.json b/packages/code-analyzer-sfge-engine/package.json index c367b111..2bd7a922 100644 --- a/packages/code-analyzer-sfge-engine/package.json +++ b/packages/code-analyzer-sfge-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-sfge-engine", "description": "Plugin package that adds 'Salesforce Graph Engine' as an engine into Salesforce Code Analyzer", - "version": "0.11.0", + "version": "0.12.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -13,7 +13,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "dependencies": { - "@salesforce/code-analyzer-engine-api": "0.27.0", + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT", "@types/node": "^20.0.0", "semver": "^7.7.2" }, @@ -71,4 +71,4 @@ "!src/index.ts" ] } -} \ No newline at end of file +} diff --git a/packages/code-analyzer-stylelint-engine/package.json b/packages/code-analyzer-stylelint-engine/package.json index 8c6c241a..84b6ad77 100644 --- a/packages/code-analyzer-stylelint-engine/package.json +++ b/packages/code-analyzer-stylelint-engine/package.json @@ -14,7 +14,7 @@ "types": "dist/index.d.ts", "dependencies": { "@types/node": "^20.0.0", - "@salesforce/code-analyzer-engine-api": "0.27.0" + "@salesforce/code-analyzer-engine-api": "0.28.0-SNAPSHOT" }, "devDependencies": { "@eslint/js": "^9.32.0", @@ -60,4 +60,4 @@ "!src/index.ts" ] } -} \ No newline at end of file +} From 2433b75fc589aeb604311fd677b37fb5fb0a3ae3 Mon Sep 17 00:00:00 2001 From: Joshua Feingold Date: Fri, 22 Aug 2025 13:02:59 -0500 Subject: [PATCH 2/5] NEW @W-17875752@ Core now creates working directories --- .../code-analyzer-core/src/code-analyzer.ts | 60 +++++++++++++------ packages/code-analyzer-core/src/utils.ts | 20 +++++++ .../test/code-analyzer.test.ts | 24 ++++++-- .../test/rule-selection.test.ts | 17 ++++-- .../code-analyzer-core/test/test-helpers.ts | 39 +++++++++++- 5 files changed, 130 insertions(+), 30 deletions(-) diff --git a/packages/code-analyzer-core/src/code-analyzer.ts b/packages/code-analyzer-core/src/code-analyzer.ts index 8342f925..9fa47853 100644 --- a/packages/code-analyzer-core/src/code-analyzer.ts +++ b/packages/code-analyzer-core/src/code-analyzer.ts @@ -1,4 +1,5 @@ import {pathToFileURL} from "node:url"; +import os from "node:os"; import {RuleImpl, RuleSelection, RuleSelectionImpl} from "./rules" import { EngineRunResults, @@ -24,7 +25,13 @@ import * as engApi from "@salesforce/code-analyzer-engine-api" import {Clock, RealClock} from '@salesforce/code-analyzer-engine-api/utils'; import {EventEmitter} from "node:events"; import {CodeAnalyzerConfig, ConfigDescription, EngineOverrides, FIELDS, RuleOverride} from "./config"; -import {EngineProgressAggregator, RuntimeUniqueIdGenerator, toAbsolutePath, UniqueIdGenerator} from "./utils"; +import { + EngineProgressAggregator, + FileSystemHandler, + RuntimeFileSystemHandler, + RuntimeUniqueIdGenerator, + toAbsolutePath, + UniqueIdGenerator} from "./utils"; import fs from "node:fs"; import path from 'node:path'; @@ -95,6 +102,7 @@ export class CodeAnalyzer { private readonly config: CodeAnalyzerConfig; private clock: Clock = new RealClock(); private uniqueIdGenerator: UniqueIdGenerator = new RuntimeUniqueIdGenerator(); + private fileSystemHandler: FileSystemHandler = new RuntimeFileSystemHandler(); private readonly eventEmitter: EventEmitter = new EventEmitter(); private readonly engines: Map = new Map(); private readonly uninstantiableEnginesMap: Map = new Map(); @@ -124,6 +132,9 @@ export class CodeAnalyzer { _setUniqueIdGenerator(uniqueIdGenerator: UniqueIdGenerator) { this.uniqueIdGenerator = uniqueIdGenerator; } + _setFileSystemHandler(fileSystemHandler: FileSystemHandler): void { + this.fileSystemHandler = fileSystemHandler; + } /** * Convenience method to return the same CodeAnalyzerConfig instance that was provided to the constructor @@ -296,15 +307,16 @@ export class CodeAnalyzer { // called a second time before the first call to run hasn't finished. This can occur if someone builds // up a bunch of RunResults promises and then does a Promise.all on them. Otherwise, the progress events may // override each other. - - const engineRunOptions: engApi.RunOptions = extractEngineRunOptions(runOptions, this.config.getLogFolder()); + const tmpDirRoot: string = path.join(os.tmpdir(), `code-analyzer`, `run-${this.clock.formatToDateTimeString()}`); + await this.fileSystemHandler.createDirectory(tmpDirRoot); + const workspace: engApi.Workspace = toEngApiWorkspace(runOptions.workspace); this.emitLogEvent(LogLevel.Debug, getMessage('RunningWithWorkspace', JSON.stringify({ filesAndFolders: runOptions.workspace.getRawFilesAndFolders(), targets: runOptions.workspace.getRawTargets() }))); const runPromises: Promise[] = ruleSelection.getEngineNames().map( - engineName => this.runEngineAndValidateResults(engineName, ruleSelection, engineRunOptions)); + engineName => this.runEngineAndValidateResults(engineName, ruleSelection, this.config.getLogFolder(), workspace, tmpDirRoot)); const engineRunResultsList: EngineRunResults[] = await Promise.all(runPromises); const runResults: RunResultsImpl = new RunResultsImpl(this.clock); @@ -314,6 +326,7 @@ export class CodeAnalyzer { for (const [uninstantiableEngine, error] of this.uninstantiableEnginesMap.entries()) { runResults.addEngineRunResults(new UninstantiableEngineRunResults(uninstantiableEngine, error)); } + await this.fileSystemHandler.deleteDirectory(tmpDirRoot); return runResults; } @@ -331,25 +344,33 @@ export class CodeAnalyzer { private async getAllRules(workspace?: Workspace): Promise { const cacheKey: string = workspace ? workspace.getWorkspaceId() : process.cwd(); if (!this.rulesCache.has(cacheKey)) { + // TODO: THIS WILL BE CONFIGURABLE SOON. + const tmpDirRoot: string = path.join(os.tmpdir(), `code-analyzer`, `describe-${this.clock.formatToDateTimeString()}`); + await this.fileSystemHandler.createDirectory(tmpDirRoot); this.engineRuleDiscoveryProgressAggregator.reset(this.getEngineNames()); const engApiWorkspace: engApi.Workspace | undefined = workspace ? toEngApiWorkspace(workspace) : undefined; const rulePromises: Promise[] = this.getEngineNames().map(engineName => - this.getAllRulesFor(engineName, { - workspace: engApiWorkspace, - workingDirectory: '.', - logFolder: this.config.getLogFolder() - })); + this.getAllRulesFor(engineName, engApiWorkspace, tmpDirRoot, this.config.getLogFolder())); this.rulesCache.set(cacheKey, (await Promise.all(rulePromises)).flat()); + await this.fileSystemHandler.deleteDirectory(tmpDirRoot); } return this.rulesCache.get(cacheKey)!; } - private async getAllRulesFor(engineName: string, describeOptions: engApi.DescribeOptions): Promise { + private async getAllRulesFor(engineName: string, workspace: engApi.Workspace | undefined, tmpDirRoot: string, logFolder: string): Promise { + const workingDirectory: string = path.join(tmpDirRoot, engineName); + await this.fileSystemHandler.createDirectory(workingDirectory); + const describeOptions: engApi.DescribeOptions = { + workspace, + workingDirectory, + logFolder + }; this.emitLogEvent(LogLevel.Debug, getMessage('GatheringRulesFromEngine', engineName)); let ruleDescriptions: engApi.RuleDescription[] = []; try { ruleDescriptions = await this.getEngine(engineName).describeRules(describeOptions); + await this.fileSystemHandler.deleteDirectory(workingDirectory); } catch (err) { this.uninstantiableEnginesMap.set(engineName, err as Error); this.emitLogEvent(LogLevel.Error, getMessage('PluginErrorWhenGettingRules', engineName, (err as Error).message + '\n\n' + @@ -371,7 +392,14 @@ export class CodeAnalyzer { this.emitEvent({type: EventType.RuleSelectionProgressEvent, timestamp: this.clock.now(), percentComplete: aggregatedPerc}); } - private async runEngineAndValidateResults(engineName: string, ruleSelection: RuleSelection, engineRunOptions: engApi.RunOptions): Promise { + private async runEngineAndValidateResults(engineName: string, ruleSelection: RuleSelection, logFolder: string, workspace: engApi.Workspace, tmpDirRoot: string): Promise { + const workingDirectory: string = path.join(tmpDirRoot, engineName); + await this.fileSystemHandler.createDirectory(workingDirectory); + const engineRunOptions: engApi.RunOptions = { + logFolder, + workspace, + workingDirectory + }; this.emitEvent({ type: EventType.EngineRunProgressEvent, timestamp: this.clock.now(), engineName: engineName, percentComplete: 0 }); @@ -383,6 +411,7 @@ export class CodeAnalyzer { let apiEngineRunResults: engApi.EngineRunResults; try { apiEngineRunResults = await engine.runRules(rulesToRun, engineRunOptions); + await this.fileSystemHandler.deleteDirectory(workingDirectory); } catch (error) { return new UnexpectedErrorEngineRunResults(engineName, await engine.getEngineVersion(), error as Error); } @@ -607,14 +636,6 @@ function validateRuleDescriptions(ruleDescriptions: engApi.RuleDescription[], en } } -function extractEngineRunOptions(runOptions: RunOptions, logFolder: string): engApi.RunOptions { - return { - logFolder: logFolder, - workingDirectory: '.', - workspace: toEngApiWorkspace(runOptions.workspace), - }; -} - async function validateFileOrFolder(fileOrFolder: string): Promise { const absFileOrFolder: string = toAbsolutePath(fileOrFolder); try { @@ -698,6 +719,7 @@ function validateViolationCodeLocations(violation: engApi.Violation, engineName: engineName, violation.ruleName, codeLocation.endLine, codeLocation.startLine)); } + // istanbul ignore else if (codeLocation.endColumn !== undefined) { if (!isValidLineOrColumn(codeLocation.endColumn)) { throw new Error(getMessage('EngineReturnedViolationWithCodeLocationWithInvalidLineOrColumn', diff --git a/packages/code-analyzer-core/src/utils.ts b/packages/code-analyzer-core/src/utils.ts index 15f0dcbe..ae12f991 100644 --- a/packages/code-analyzer-core/src/utils.ts +++ b/packages/code-analyzer-core/src/utils.ts @@ -1,5 +1,6 @@ import * as path from "node:path"; import * as crypto from "node:crypto"; +import fs from "node:fs"; // THIS FILE CONTAINS UTILITIES WHICH ARE USED INTERNALLY ONLY. // None of the following exported interfaces and functions should be exported from the index file. @@ -9,6 +10,25 @@ export function toAbsolutePath(fileOrFolder: string): string { return path.resolve(fileOrFolder.replace(/[\\/]/g, path.sep)); } +export interface FileSystemHandler { + createDirectory(absolutePath: string): Promise; + deleteDirectory(absolutePath: string): Promise; +} + +export class RuntimeFileSystemHandler implements FileSystemHandler { + async createDirectory(absolutePath: string): Promise { + await fs.promises.mkdir(absolutePath, { + recursive: true + }); + } + + async deleteDirectory(absolutePath: string): Promise { + await fs.promises.rm(absolutePath, { + recursive: true, + }); + } +} + export interface UniqueIdGenerator { getLocallyUniqueId(prefix: string): string; diff --git a/packages/code-analyzer-core/test/code-analyzer.test.ts b/packages/code-analyzer-core/test/code-analyzer.test.ts index d06ac011..d8f628ef 100644 --- a/packages/code-analyzer-core/test/code-analyzer.test.ts +++ b/packages/code-analyzer-core/test/code-analyzer.test.ts @@ -21,7 +21,7 @@ import * as stubs from "./stubs"; import {getMessage} from "../src/messages"; import os from "node:os"; import path from "node:path"; -import {changeWorkingDirectoryToPackageRoot, FixedUniqueIdGenerator} from "./test-helpers"; +import {changeWorkingDirectoryToPackageRoot, FakeFileSystemHandler, FixedUniqueIdGenerator} from "./test-helpers"; import * as engApi from "@salesforce/code-analyzer-engine-api" import {FixedClock} from "@salesforce/code-analyzer-engine-api/utils" import {UnexpectedEngineErrorRule} from "../src/rules"; @@ -188,6 +188,7 @@ describe("Tests for the run method of CodeAnalyzer", () => { let stubEngine2: stubs.StubEngine2; let selection: RuleSelection; let fixedClock: FixedClock; + let fakeFileSystemHandler: FakeFileSystemHandler; const expectedStubEngine1RuleNames: string[] = ['stub1RuleA', 'stub1RuleB', 'stub1RuleC']; const expectedStubEngine2RuleNames: string[] = ['stub2RuleA', 'stub2RuleC']; @@ -195,8 +196,10 @@ describe("Tests for the run method of CodeAnalyzer", () => { sampleTimestamp = new Date(); codeAnalyzer = new CodeAnalyzer(CodeAnalyzerConfig.withDefaults()); fixedClock = new FixedClock(sampleTimestamp); + fakeFileSystemHandler = new FakeFileSystemHandler(); codeAnalyzer._setClock(fixedClock); codeAnalyzer._setUniqueIdGenerator(new FixedUniqueIdGenerator()); + codeAnalyzer._setFileSystemHandler(fakeFileSystemHandler); sampleRunOptions = {workspace: await codeAnalyzer.createWorkspace([__dirname])}; const stubPlugin: stubs.StubEnginePlugin = new stubs.StubEnginePlugin(); await codeAnalyzer.addEnginePlugin(stubPlugin); @@ -212,30 +215,38 @@ describe("Tests for the run method of CodeAnalyzer", () => { ]), }); - const workingDirectoriesRoot: string = `code-analyzer-${fixedClock.formatToDateTimeString()}`; + const workingDirectoriesRoot: string = path.join(os.tmpdir(), `code-analyzer`, `run-${fixedClock.formatToDateTimeString()}`); const expectedEngineRunOptionsEngine1: engApi.RunOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), - workingDirectory: path.join(os.tmpdir(), workingDirectoriesRoot, 'run', 'stubEngine1'), + workingDirectory: path.join(workingDirectoriesRoot, 'stubEngine1'), workspace: new engApi.Workspace("FixedId", [SAMPLE_WORKSPACE_FOLDER], [ path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')]) }; const expectedEngineRunOptionsEngine2: engApi.RunOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), - workingDirectory: path.join(os.tmpdir(), workingDirectoriesRoot, 'run', 'stubEngine2'), + workingDirectory: path.join(workingDirectoriesRoot, 'stubEngine2'), workspace: new engApi.Workspace("FixedId", [SAMPLE_WORKSPACE_FOLDER], [ path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')]) }; + expect(fakeFileSystemHandler.dirWasCreated(workingDirectoriesRoot)).toEqual(true); + expect(fakeFileSystemHandler.dirWasDeleted(workingDirectoriesRoot)).toEqual(true); expect(stubEngine1.runRulesCallHistory).toHaveLength(1); expect(stubEngine1.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine1RuleNames); expectEquivalentRunOptions(stubEngine1.runRulesCallHistory[0].runOptions, expectedEngineRunOptionsEngine1); + expect(fakeFileSystemHandler.dirWasCreated(expectedEngineRunOptionsEngine1.workingDirectory)).toEqual(true); + expect(fakeFileSystemHandler.dirWasDeleted(expectedEngineRunOptionsEngine1.workingDirectory)).toEqual(true); expect(stubEngine2.runRulesCallHistory).toHaveLength(1); expect(stubEngine2.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine2RuleNames); expectEquivalentRunOptions(stubEngine2.runRulesCallHistory[0].runOptions, expectedEngineRunOptionsEngine2); + expect(fakeFileSystemHandler.dirWasCreated(expectedEngineRunOptionsEngine2.workingDirectory)).toEqual(true); + expect(fakeFileSystemHandler.dirWasDeleted(expectedEngineRunOptionsEngine2.workingDirectory)).toEqual(true); }); it("When the workspace provided is one that is not constructed from CodeAnalyzer's createWorkspace method, then it should still work", async () => { const dummyWorkspace: Workspace = new StubWorkspace(); + const workingDirectoriesRoot: string = path.join(os.tmpdir(), `code-analyzer`, `run-${fixedClock.formatToDateTimeString()}`); + await codeAnalyzer.run(selection, { workspace: dummyWorkspace }); @@ -244,6 +255,7 @@ describe("Tests for the run method of CodeAnalyzer", () => { ruleNames: expectedStubEngine1RuleNames, runOptions: { logFolder: codeAnalyzer.getConfig().getLogFolder(), + workingDirectory: path.join(workingDirectoriesRoot, 'stubEngine1'), workspace: new engApi.Workspace(dummyWorkspace.getWorkspaceId(), dummyWorkspace.getRawFilesAndFolders(), dummyWorkspace.getRawTargets()) } @@ -254,11 +266,11 @@ describe("Tests for the run method of CodeAnalyzer", () => { selection = await codeAnalyzer.selectRules(['stubEngine1:Recommended']); await codeAnalyzer.run(selection, sampleRunOptions); - const workingDirectoriesRoot: string = `code-analyzer-${fixedClock.formatToDateTimeString()}`; + const workingDirectoriesRoot: string = path.join(os.tmpdir(), `code-analyzer`, `run-${fixedClock.formatToDateTimeString()}`); const expectedEngineRunOptions: engApi.RunOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), - workingDirectory: path.join(os.tmpdir(), workingDirectoriesRoot, 'run', 'stubEngine1'), + workingDirectory: path.join(workingDirectoriesRoot, 'stubEngine1'), workspace: new engApi.Workspace("FixedId", [__dirname]) }; expect(stubEngine1.runRulesCallHistory).toHaveLength(1); diff --git a/packages/code-analyzer-core/test/rule-selection.test.ts b/packages/code-analyzer-core/test/rule-selection.test.ts index 63a7b751..ce45c18b 100644 --- a/packages/code-analyzer-core/test/rule-selection.test.ts +++ b/packages/code-analyzer-core/test/rule-selection.test.ts @@ -17,7 +17,7 @@ import {FixedClock} from "@salesforce/code-analyzer-engine-api/utils"; import {RepeatedRuleNameEnginePlugin, StubEnginePlugin} from "./stubs"; import path from "node:path"; import os from "node:os"; -import {changeWorkingDirectoryToPackageRoot, FixedUniqueIdGenerator} from "./test-helpers"; +import {changeWorkingDirectoryToPackageRoot, FakeFileSystemHandler, FixedUniqueIdGenerator} from "./test-helpers"; import {getMessage} from "../src/messages"; import * as stubs from "./stubs"; @@ -28,6 +28,7 @@ describe('Tests for selecting rules', () => { let plugin: StubEnginePlugin; let sampleTimestamp: Date; let fixedClock: FixedClock; + let fakeFileSystemHandler: FakeFileSystemHandler; async function setupCodeAnalyzer(codeAnalyzer: CodeAnalyzer) : Promise { plugin = new StubEnginePlugin(); @@ -41,6 +42,8 @@ describe('Tests for selecting rules', () => { sampleTimestamp = new Date(); fixedClock = new FixedClock(sampleTimestamp); codeAnalyzer._setClock(fixedClock); + fakeFileSystemHandler = new FakeFileSystemHandler(); + codeAnalyzer._setFileSystemHandler(fakeFileSystemHandler); }) it('When no rule selectors are provided then the Recommended tag is used', async () => { @@ -272,22 +275,28 @@ describe('Tests for selecting rules', () => { it('When selectRules is not provided with SelectOptions, then workspace should be undefined for all engines', async () => { await codeAnalyzer.selectRules(['all']); - const workingDirectoriesRoot: string = `code-analyzer-${fixedClock.formatToDateTimeString()}`; + const workingDirectoriesRoot: string = path.join(os.tmpdir(), `code-analyzer`, `describe-${fixedClock.formatToDateTimeString()}`); const expectedDescribeOptionsEngine1: engApi.DescribeOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), - workingDirectory: path.join(os.tmpdir(), workingDirectoriesRoot, 'describe', 'stubEngine1'), + workingDirectory: path.join(workingDirectoriesRoot, 'stubEngine1'), workspace: undefined }; const expectedDescribeOptionsEngine2: engApi.DescribeOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), - workingDirectory: path.join(os.tmpdir(), workingDirectoriesRoot, 'describe', 'stubEngine2'), + workingDirectory: path.join(workingDirectoriesRoot, 'stubEngine2'), workspace: undefined }; + expect(fakeFileSystemHandler.dirWasCreated(workingDirectoriesRoot)).toEqual(true); + expect(fakeFileSystemHandler.dirWasDeleted(workingDirectoriesRoot)).toEqual(true); const stubEngine1: stubs.StubEngine1 = plugin.getCreatedEngine('stubEngine1') as stubs.StubEngine1; expect(stubEngine1.describeRulesCallHistory).toEqual([{describeOptions: expectedDescribeOptionsEngine1}]); + expect(fakeFileSystemHandler.dirWasCreated(expectedDescribeOptionsEngine1.workingDirectory)).toEqual(true); + expect(fakeFileSystemHandler.dirWasDeleted(expectedDescribeOptionsEngine1.workingDirectory)).toEqual(true); const stubEngine2: stubs.StubEngine2 = plugin.getCreatedEngine('stubEngine2') as stubs.StubEngine2; expect(stubEngine2.describeRulesCallHistory).toEqual([{describeOptions: expectedDescribeOptionsEngine2}]); + expect(fakeFileSystemHandler.dirWasCreated(expectedDescribeOptionsEngine2.workingDirectory)).toEqual(true); + expect(fakeFileSystemHandler.dirWasDeleted(expectedDescribeOptionsEngine2.workingDirectory)).toEqual(true); }); it('When selectRules is provided with SelectOptions, then they are forwarded to the engines', async () => { diff --git a/packages/code-analyzer-core/test/test-helpers.ts b/packages/code-analyzer-core/test/test-helpers.ts index 87b89f3b..31446c9f 100644 --- a/packages/code-analyzer-core/test/test-helpers.ts +++ b/packages/code-analyzer-core/test/test-helpers.ts @@ -1,6 +1,6 @@ import process from "node:process"; import path from "node:path"; -import {UniqueIdGenerator} from "../src/utils"; +import {FileSystemHandler, UniqueIdGenerator} from "../src/utils"; export function changeWorkingDirectoryToPackageRoot() { let original_working_directory: string; @@ -27,3 +27,40 @@ export class FixedUniqueIdGenerator implements UniqueIdGenerator { return "FixedUUID"; } } + +export class FakeFileSystemHandler implements FileSystemHandler { + private fsMap: Map = new Map(); + + createDirectory(absolutePath: string): Promise { + if (this.fsMap.has(absolutePath)) { + throw new Error(`TEST ERROR: Path ${absolutePath} was created twice`); + } + this.fsMap.set(absolutePath, { + created: true, + deleted: false + }); + return Promise.resolve(); + } + + deleteDirectory(absolutePath: string): Promise { + if (!this.fsMap.has(absolutePath)) { + throw new Error(`TEST ERROR: Path ${absolutePath} was deleted without being created`); + } + if (this.fsMap.get(absolutePath)!.deleted) { + throw new Error(`TEST ERROR: Path ${absolutePath} was deleted twice`); + } + this.fsMap.get(absolutePath)!.deleted = true; + return Promise.resolve(); + } + + dirWasCreated(absolutePath: string): boolean { + return this.fsMap.has(absolutePath) && this.fsMap.get(absolutePath)!.created; + } + + dirWasDeleted(absolutePath: string): boolean { + return this.fsMap.has(absolutePath) && this.fsMap.get(absolutePath)!.deleted; + } +} From a3fe9b1fcff908d4eda4c6698273cc5abc2bb321 Mon Sep 17 00:00:00 2001 From: Joshua Feingold Date: Tue, 26 Aug 2025 11:38:50 -0500 Subject: [PATCH 3/5] NEW @W-17875752@ Renamed property to workingFolder --- packages/code-analyzer-core/src/code-analyzer.ts | 16 ++++++++-------- .../test/code-analyzer.test.ts | 16 ++++++++-------- .../test/rule-selection.test.ts | 12 ++++++------ packages/code-analyzer-engine-api/src/engines.ts | 4 ++-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/code-analyzer-core/src/code-analyzer.ts b/packages/code-analyzer-core/src/code-analyzer.ts index 9fa47853..c1940231 100644 --- a/packages/code-analyzer-core/src/code-analyzer.ts +++ b/packages/code-analyzer-core/src/code-analyzer.ts @@ -359,18 +359,18 @@ export class CodeAnalyzer { } private async getAllRulesFor(engineName: string, workspace: engApi.Workspace | undefined, tmpDirRoot: string, logFolder: string): Promise { - const workingDirectory: string = path.join(tmpDirRoot, engineName); - await this.fileSystemHandler.createDirectory(workingDirectory); + const workingFolder: string = path.join(tmpDirRoot, engineName); + await this.fileSystemHandler.createDirectory(workingFolder); const describeOptions: engApi.DescribeOptions = { workspace, - workingDirectory, + workingFolder, logFolder }; this.emitLogEvent(LogLevel.Debug, getMessage('GatheringRulesFromEngine', engineName)); let ruleDescriptions: engApi.RuleDescription[] = []; try { ruleDescriptions = await this.getEngine(engineName).describeRules(describeOptions); - await this.fileSystemHandler.deleteDirectory(workingDirectory); + await this.fileSystemHandler.deleteDirectory(workingFolder); } catch (err) { this.uninstantiableEnginesMap.set(engineName, err as Error); this.emitLogEvent(LogLevel.Error, getMessage('PluginErrorWhenGettingRules', engineName, (err as Error).message + '\n\n' + @@ -393,12 +393,12 @@ export class CodeAnalyzer { } private async runEngineAndValidateResults(engineName: string, ruleSelection: RuleSelection, logFolder: string, workspace: engApi.Workspace, tmpDirRoot: string): Promise { - const workingDirectory: string = path.join(tmpDirRoot, engineName); - await this.fileSystemHandler.createDirectory(workingDirectory); + const workingFolder: string = path.join(tmpDirRoot, engineName); + await this.fileSystemHandler.createDirectory(workingFolder); const engineRunOptions: engApi.RunOptions = { logFolder, workspace, - workingDirectory + workingFolder }; this.emitEvent({ type: EventType.EngineRunProgressEvent, timestamp: this.clock.now(), engineName: engineName, percentComplete: 0 @@ -411,7 +411,7 @@ export class CodeAnalyzer { let apiEngineRunResults: engApi.EngineRunResults; try { apiEngineRunResults = await engine.runRules(rulesToRun, engineRunOptions); - await this.fileSystemHandler.deleteDirectory(workingDirectory); + await this.fileSystemHandler.deleteDirectory(workingFolder); } catch (error) { return new UnexpectedErrorEngineRunResults(engineName, await engine.getEngineVersion(), error as Error); } diff --git a/packages/code-analyzer-core/test/code-analyzer.test.ts b/packages/code-analyzer-core/test/code-analyzer.test.ts index d8f628ef..974de172 100644 --- a/packages/code-analyzer-core/test/code-analyzer.test.ts +++ b/packages/code-analyzer-core/test/code-analyzer.test.ts @@ -219,13 +219,13 @@ describe("Tests for the run method of CodeAnalyzer", () => { const expectedEngineRunOptionsEngine1: engApi.RunOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), - workingDirectory: path.join(workingDirectoriesRoot, 'stubEngine1'), + workingFolder: path.join(workingDirectoriesRoot, 'stubEngine1'), workspace: new engApi.Workspace("FixedId", [SAMPLE_WORKSPACE_FOLDER], [ path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')]) }; const expectedEngineRunOptionsEngine2: engApi.RunOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), - workingDirectory: path.join(workingDirectoriesRoot, 'stubEngine2'), + workingFolder: path.join(workingDirectoriesRoot, 'stubEngine2'), workspace: new engApi.Workspace("FixedId", [SAMPLE_WORKSPACE_FOLDER], [ path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')]) }; @@ -234,13 +234,13 @@ describe("Tests for the run method of CodeAnalyzer", () => { expect(stubEngine1.runRulesCallHistory).toHaveLength(1); expect(stubEngine1.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine1RuleNames); expectEquivalentRunOptions(stubEngine1.runRulesCallHistory[0].runOptions, expectedEngineRunOptionsEngine1); - expect(fakeFileSystemHandler.dirWasCreated(expectedEngineRunOptionsEngine1.workingDirectory)).toEqual(true); - expect(fakeFileSystemHandler.dirWasDeleted(expectedEngineRunOptionsEngine1.workingDirectory)).toEqual(true); + expect(fakeFileSystemHandler.dirWasCreated(expectedEngineRunOptionsEngine1.workingFolder)).toEqual(true); + expect(fakeFileSystemHandler.dirWasDeleted(expectedEngineRunOptionsEngine1.workingFolder)).toEqual(true); expect(stubEngine2.runRulesCallHistory).toHaveLength(1); expect(stubEngine2.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine2RuleNames); expectEquivalentRunOptions(stubEngine2.runRulesCallHistory[0].runOptions, expectedEngineRunOptionsEngine2); - expect(fakeFileSystemHandler.dirWasCreated(expectedEngineRunOptionsEngine2.workingDirectory)).toEqual(true); - expect(fakeFileSystemHandler.dirWasDeleted(expectedEngineRunOptionsEngine2.workingDirectory)).toEqual(true); + expect(fakeFileSystemHandler.dirWasCreated(expectedEngineRunOptionsEngine2.workingFolder)).toEqual(true); + expect(fakeFileSystemHandler.dirWasDeleted(expectedEngineRunOptionsEngine2.workingFolder)).toEqual(true); }); it("When the workspace provided is one that is not constructed from CodeAnalyzer's createWorkspace method, then it should still work", async () => { @@ -255,7 +255,7 @@ describe("Tests for the run method of CodeAnalyzer", () => { ruleNames: expectedStubEngine1RuleNames, runOptions: { logFolder: codeAnalyzer.getConfig().getLogFolder(), - workingDirectory: path.join(workingDirectoriesRoot, 'stubEngine1'), + workingFolder: path.join(workingDirectoriesRoot, 'stubEngine1'), workspace: new engApi.Workspace(dummyWorkspace.getWorkspaceId(), dummyWorkspace.getRawFilesAndFolders(), dummyWorkspace.getRawTargets()) } @@ -270,7 +270,7 @@ describe("Tests for the run method of CodeAnalyzer", () => { const expectedEngineRunOptions: engApi.RunOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), - workingDirectory: path.join(workingDirectoriesRoot, 'stubEngine1'), + workingFolder: path.join(workingDirectoriesRoot, 'stubEngine1'), workspace: new engApi.Workspace("FixedId", [__dirname]) }; expect(stubEngine1.runRulesCallHistory).toHaveLength(1); diff --git a/packages/code-analyzer-core/test/rule-selection.test.ts b/packages/code-analyzer-core/test/rule-selection.test.ts index ce45c18b..f2fdc670 100644 --- a/packages/code-analyzer-core/test/rule-selection.test.ts +++ b/packages/code-analyzer-core/test/rule-selection.test.ts @@ -279,24 +279,24 @@ describe('Tests for selecting rules', () => { const expectedDescribeOptionsEngine1: engApi.DescribeOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), - workingDirectory: path.join(workingDirectoriesRoot, 'stubEngine1'), + workingFolder: path.join(workingDirectoriesRoot, 'stubEngine1'), workspace: undefined }; const expectedDescribeOptionsEngine2: engApi.DescribeOptions = { logFolder: codeAnalyzer.getConfig().getLogFolder(), - workingDirectory: path.join(workingDirectoriesRoot, 'stubEngine2'), + workingFolder: path.join(workingDirectoriesRoot, 'stubEngine2'), workspace: undefined }; expect(fakeFileSystemHandler.dirWasCreated(workingDirectoriesRoot)).toEqual(true); expect(fakeFileSystemHandler.dirWasDeleted(workingDirectoriesRoot)).toEqual(true); const stubEngine1: stubs.StubEngine1 = plugin.getCreatedEngine('stubEngine1') as stubs.StubEngine1; expect(stubEngine1.describeRulesCallHistory).toEqual([{describeOptions: expectedDescribeOptionsEngine1}]); - expect(fakeFileSystemHandler.dirWasCreated(expectedDescribeOptionsEngine1.workingDirectory)).toEqual(true); - expect(fakeFileSystemHandler.dirWasDeleted(expectedDescribeOptionsEngine1.workingDirectory)).toEqual(true); + expect(fakeFileSystemHandler.dirWasCreated(expectedDescribeOptionsEngine1.workingFolder)).toEqual(true); + expect(fakeFileSystemHandler.dirWasDeleted(expectedDescribeOptionsEngine1.workingFolder)).toEqual(true); const stubEngine2: stubs.StubEngine2 = plugin.getCreatedEngine('stubEngine2') as stubs.StubEngine2; expect(stubEngine2.describeRulesCallHistory).toEqual([{describeOptions: expectedDescribeOptionsEngine2}]); - expect(fakeFileSystemHandler.dirWasCreated(expectedDescribeOptionsEngine2.workingDirectory)).toEqual(true); - expect(fakeFileSystemHandler.dirWasDeleted(expectedDescribeOptionsEngine2.workingDirectory)).toEqual(true); + expect(fakeFileSystemHandler.dirWasCreated(expectedDescribeOptionsEngine2.workingFolder)).toEqual(true); + expect(fakeFileSystemHandler.dirWasDeleted(expectedDescribeOptionsEngine2.workingFolder)).toEqual(true); }); it('When selectRules is provided with SelectOptions, then they are forwarded to the engines', async () => { diff --git a/packages/code-analyzer-engine-api/src/engines.ts b/packages/code-analyzer-engine-api/src/engines.ts index 4921cca7..19cf03cb 100644 --- a/packages/code-analyzer-engine-api/src/engines.ts +++ b/packages/code-analyzer-engine-api/src/engines.ts @@ -22,7 +22,7 @@ export type DescribeOptions = { * A temporary folder created specifically for use by the current engine. If the engine ever needs to create temporary * files or folders, that should be done here. */ - workingDirectory: string + workingFolder: string /** * The workspace may or may not be available. If available, then engines should use this workspace object to give a @@ -51,7 +51,7 @@ export type RunOptions = { * A temporary folder created specifically for use by the current engine. If the engine ever needs to create temporary * files or folders, that should be done here. */ - workingDirectory: string + workingFolder: string /** * The workspace object specifying the files that make up the user's workspace and which files should be targeted. From 08835f73c22607ea6b901b53b040c7e4250b3bf9 Mon Sep 17 00:00:00 2001 From: Joshua Feingold Date: Tue, 26 Aug 2025 14:07:46 -0500 Subject: [PATCH 4/5] NEW @W-17875752@ Reverted to use of tmp module --- package-lock.json | 8 +++--- packages/code-analyzer-core/src/utils.ts | 28 ++++++++++++++----- .../code-analyzer-engine-api/package.json | 2 +- .../src/utils/fs-utils.ts | 4 +++ .../src/utils/index.ts | 1 + .../test/utils/utils.test.ts | 11 +++++++- 6 files changed, 41 insertions(+), 13 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8b1da131..d1e60ff0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7958,9 +7958,9 @@ "license": "MIT" }, "node_modules/tmp": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", - "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", "license": "MIT", "engines": { "node": ">=14.14" @@ -9037,7 +9037,7 @@ "license": "BSD-3-Clause", "dependencies": { "@types/node": "^20.0.0", - "tmp": "^0.2.3" + "tmp": "^0.2.5" }, "devDependencies": { "@eslint/js": "^9.32.0", diff --git a/packages/code-analyzer-core/src/utils.ts b/packages/code-analyzer-core/src/utils.ts index ae12f991..1bedf821 100644 --- a/packages/code-analyzer-core/src/utils.ts +++ b/packages/code-analyzer-core/src/utils.ts @@ -1,6 +1,7 @@ import * as path from "node:path"; import * as crypto from "node:crypto"; import fs from "node:fs"; +import { createNamedTempDir } from "@salesforce/code-analyzer-engine-api/utils"; // THIS FILE CONTAINS UTILITIES WHICH ARE USED INTERNALLY ONLY. // None of the following exported interfaces and functions should be exported from the index file. @@ -16,16 +17,29 @@ export interface FileSystemHandler { } export class RuntimeFileSystemHandler implements FileSystemHandler { + async createDirectory(absolutePath: string): Promise { - await fs.promises.mkdir(absolutePath, { - recursive: true - }); + const directories: string[] = this.breakPathIntoDirectoryArray(absolutePath); + for (const directory of directories) { + if (!fs.existsSync(directory)) { + await createNamedTempDir(path.basename(directory), path.dirname(directory)); + } + } + } + + private breakPathIntoDirectoryArray(absolutePath: string): string[] { + const directoryArray: string[] = []; + let currentDir: string = absolutePath; + do { + directoryArray.unshift(currentDir); + currentDir = path.dirname(currentDir); + } while (currentDir && currentDir != directoryArray[0]); + return directoryArray; } - async deleteDirectory(absolutePath: string): Promise { - await fs.promises.rm(absolutePath, { - recursive: true, - }); + deleteDirectory(_absolutePath: string): Promise { + // CURRENTLY DELIBERATE NO-OP, BECAUSE THE DIRECTORIES SHOULD CLEAN THEMSELVES UP. + return Promise.resolve(); } } diff --git a/packages/code-analyzer-engine-api/package.json b/packages/code-analyzer-engine-api/package.json index 09a03af6..49ee7310 100644 --- a/packages/code-analyzer-engine-api/package.json +++ b/packages/code-analyzer-engine-api/package.json @@ -17,7 +17,7 @@ "types": "dist/index.d.ts", "dependencies": { "@types/node": "^20.0.0", - "tmp": "^0.2.3" + "tmp": "^0.2.5" }, "devDependencies": { "@eslint/js": "^9.32.0", diff --git a/packages/code-analyzer-engine-api/src/utils/fs-utils.ts b/packages/code-analyzer-engine-api/src/utils/fs-utils.ts index 9f7ce20a..6dcb64a7 100644 --- a/packages/code-analyzer-engine-api/src/utils/fs-utils.ts +++ b/packages/code-analyzer-engine-api/src/utils/fs-utils.ts @@ -6,6 +6,10 @@ import fs from "node:fs"; tmp.setGracefulCleanup(); const tmpDirAsync = promisify((options: tmp.DirOptions, cb: tmp.DirCallback) => tmp.dir(options, cb)); +export async function createNamedTempDir(name: string, parentTempDir?: string): Promise { + return tmpDirAsync({name, dir: parentTempDir, keep: false, unsafeCleanup: true}); +} + /** * Creates a temporary directory that eventually cleans up after itself * @param parentTempDir - if supplied, then a temporary folder is placed directly underneath this parent folder. diff --git a/packages/code-analyzer-engine-api/src/utils/index.ts b/packages/code-analyzer-engine-api/src/utils/index.ts index f761c135..6f22981f 100644 --- a/packages/code-analyzer-engine-api/src/utils/index.ts +++ b/packages/code-analyzer-engine-api/src/utils/index.ts @@ -5,6 +5,7 @@ export { } from './datetime-utils'; export { + createNamedTempDir, createTempDir, calculateLongestCommonParentFolderOf } from './fs-utils'; diff --git a/packages/code-analyzer-engine-api/test/utils/utils.test.ts b/packages/code-analyzer-engine-api/test/utils/utils.test.ts index 077c775b..fcf70f85 100644 --- a/packages/code-analyzer-engine-api/test/utils/utils.test.ts +++ b/packages/code-analyzer-engine-api/test/utils/utils.test.ts @@ -1,6 +1,6 @@ import fs from 'node:fs'; import os from 'node:os'; -import {createTempDir, FixedClock, indent, JavaCommandExecutor, RealClock} from "../../src/utils"; +import {createTempDir, createNamedTempDir, FixedClock, indent, JavaCommandExecutor, RealClock} from "../../src/utils"; describe('Tests for Clock', () => { @@ -26,6 +26,15 @@ describe('Tests for Clock', () => { }); }) +describe('Tests for createNamedTempDir', () => { + it('Successfully creates named directory', async () => { + const timestamp: number = Date.now(); + const tmpDir: string = await createNamedTempDir(`tmp-${timestamp}`, os.tmpdir()); + + expect(fs.existsSync(tmpDir)).toEqual(true); + }); +}); + describe('Tests for createTempDir', () => { it('Successfully creates temporary directory', async () => { // Before testing, figure out how many entries are in the temp folder. From 804b12dd10926069a06c0ab17a3ca77369794f9b Mon Sep 17 00:00:00 2001 From: Joshua Feingold Date: Tue, 26 Aug 2025 15:46:14 -0500 Subject: [PATCH 5/5] NEW @W-17875752@ Feedback from code review --- .../code-analyzer-core/src/code-analyzer.ts | 20 +++++++---- packages/code-analyzer-core/src/utils.ts | 26 +++------------ .../test/code-analyzer.test.ts | 3 -- .../test/output-format.test.ts | 5 ++- .../test/rule-selection.test.ts | 3 -- .../code-analyzer-core/test/test-helpers.ts | 33 +++++-------------- 6 files changed, 31 insertions(+), 59 deletions(-) diff --git a/packages/code-analyzer-core/src/code-analyzer.ts b/packages/code-analyzer-core/src/code-analyzer.ts index c1940231..a73aa8bc 100644 --- a/packages/code-analyzer-core/src/code-analyzer.ts +++ b/packages/code-analyzer-core/src/code-analyzer.ts @@ -110,6 +110,7 @@ export class CodeAnalyzer { private readonly engineConfigDescriptions: Map = new Map(); private readonly rulesCache: Map = new Map(); private readonly engineRuleDiscoveryProgressAggregator: EngineProgressAggregator = new EngineProgressAggregator(); + private workingFolderRoot?: string; constructor(config: CodeAnalyzerConfig, version: string = process.version) { this.validateEnvironment(version); @@ -307,7 +308,7 @@ export class CodeAnalyzer { // called a second time before the first call to run hasn't finished. This can occur if someone builds // up a bunch of RunResults promises and then does a Promise.all on them. Otherwise, the progress events may // override each other. - const tmpDirRoot: string = path.join(os.tmpdir(), `code-analyzer`, `run-${this.clock.formatToDateTimeString()}`); + const tmpDirRoot: string = path.join(await this.getWorkingFolderRoot(), `run-${this.clock.formatToDateTimeString()}`); await this.fileSystemHandler.createDirectory(tmpDirRoot); const workspace: engApi.Workspace = toEngApiWorkspace(runOptions.workspace); this.emitLogEvent(LogLevel.Debug, getMessage('RunningWithWorkspace', JSON.stringify({ @@ -326,7 +327,6 @@ export class CodeAnalyzer { for (const [uninstantiableEngine, error] of this.uninstantiableEnginesMap.entries()) { runResults.addEngineRunResults(new UninstantiableEngineRunResults(uninstantiableEngine, error)); } - await this.fileSystemHandler.deleteDirectory(tmpDirRoot); return runResults; } @@ -345,7 +345,7 @@ export class CodeAnalyzer { const cacheKey: string = workspace ? workspace.getWorkspaceId() : process.cwd(); if (!this.rulesCache.has(cacheKey)) { // TODO: THIS WILL BE CONFIGURABLE SOON. - const tmpDirRoot: string = path.join(os.tmpdir(), `code-analyzer`, `describe-${this.clock.formatToDateTimeString()}`); + const tmpDirRoot: string = path.join(await this.getWorkingFolderRoot(), `describe-${this.clock.formatToDateTimeString()}`); await this.fileSystemHandler.createDirectory(tmpDirRoot); this.engineRuleDiscoveryProgressAggregator.reset(this.getEngineNames()); const engApiWorkspace: engApi.Workspace | undefined = workspace ? toEngApiWorkspace(workspace) : undefined; @@ -353,7 +353,6 @@ export class CodeAnalyzer { const rulePromises: Promise[] = this.getEngineNames().map(engineName => this.getAllRulesFor(engineName, engApiWorkspace, tmpDirRoot, this.config.getLogFolder())); this.rulesCache.set(cacheKey, (await Promise.all(rulePromises)).flat()); - await this.fileSystemHandler.deleteDirectory(tmpDirRoot); } return this.rulesCache.get(cacheKey)!; } @@ -370,7 +369,6 @@ export class CodeAnalyzer { let ruleDescriptions: engApi.RuleDescription[] = []; try { ruleDescriptions = await this.getEngine(engineName).describeRules(describeOptions); - await this.fileSystemHandler.deleteDirectory(workingFolder); } catch (err) { this.uninstantiableEnginesMap.set(engineName, err as Error); this.emitLogEvent(LogLevel.Error, getMessage('PluginErrorWhenGettingRules', engineName, (err as Error).message + '\n\n' + @@ -411,7 +409,6 @@ export class CodeAnalyzer { let apiEngineRunResults: engApi.EngineRunResults; try { apiEngineRunResults = await engine.runRules(rulesToRun, engineRunOptions); - await this.fileSystemHandler.deleteDirectory(workingFolder); } catch (error) { return new UnexpectedErrorEngineRunResults(engineName, await engine.getEngineVersion(), error as Error); } @@ -573,6 +570,17 @@ export class CodeAnalyzer { private getEngine(engineName: string): engApi.Engine { return this.engines.get(engineName)!; } + + private async getWorkingFolderRoot(): Promise { + if (!this.workingFolderRoot) { + const workingFolderRoot: string = path.join(os.tmpdir(), 'code-analyzer'); + if (!this.fileSystemHandler.directoryExists(workingFolderRoot)) { + await this.fileSystemHandler.createDirectory(workingFolderRoot); + } + this.workingFolderRoot = workingFolderRoot; + } + return this.workingFolderRoot; + } } /** diff --git a/packages/code-analyzer-core/src/utils.ts b/packages/code-analyzer-core/src/utils.ts index 1bedf821..c2f96c7a 100644 --- a/packages/code-analyzer-core/src/utils.ts +++ b/packages/code-analyzer-core/src/utils.ts @@ -12,34 +12,18 @@ export function toAbsolutePath(fileOrFolder: string): string { } export interface FileSystemHandler { + directoryExists(absolutePath: string): boolean; createDirectory(absolutePath: string): Promise; - deleteDirectory(absolutePath: string): Promise; } export class RuntimeFileSystemHandler implements FileSystemHandler { - async createDirectory(absolutePath: string): Promise { - const directories: string[] = this.breakPathIntoDirectoryArray(absolutePath); - for (const directory of directories) { - if (!fs.existsSync(directory)) { - await createNamedTempDir(path.basename(directory), path.dirname(directory)); - } - } + directoryExists(absolutePath: string): boolean { + return fs.existsSync(absolutePath); } - private breakPathIntoDirectoryArray(absolutePath: string): string[] { - const directoryArray: string[] = []; - let currentDir: string = absolutePath; - do { - directoryArray.unshift(currentDir); - currentDir = path.dirname(currentDir); - } while (currentDir && currentDir != directoryArray[0]); - return directoryArray; - } - - deleteDirectory(_absolutePath: string): Promise { - // CURRENTLY DELIBERATE NO-OP, BECAUSE THE DIRECTORIES SHOULD CLEAN THEMSELVES UP. - return Promise.resolve(); + async createDirectory(absolutePath: string): Promise { + await createNamedTempDir(path.basename(absolutePath), path.dirname(absolutePath)); } } diff --git a/packages/code-analyzer-core/test/code-analyzer.test.ts b/packages/code-analyzer-core/test/code-analyzer.test.ts index 974de172..10d71493 100644 --- a/packages/code-analyzer-core/test/code-analyzer.test.ts +++ b/packages/code-analyzer-core/test/code-analyzer.test.ts @@ -230,17 +230,14 @@ describe("Tests for the run method of CodeAnalyzer", () => { path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls')]) }; expect(fakeFileSystemHandler.dirWasCreated(workingDirectoriesRoot)).toEqual(true); - expect(fakeFileSystemHandler.dirWasDeleted(workingDirectoriesRoot)).toEqual(true); expect(stubEngine1.runRulesCallHistory).toHaveLength(1); expect(stubEngine1.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine1RuleNames); expectEquivalentRunOptions(stubEngine1.runRulesCallHistory[0].runOptions, expectedEngineRunOptionsEngine1); expect(fakeFileSystemHandler.dirWasCreated(expectedEngineRunOptionsEngine1.workingFolder)).toEqual(true); - expect(fakeFileSystemHandler.dirWasDeleted(expectedEngineRunOptionsEngine1.workingFolder)).toEqual(true); expect(stubEngine2.runRulesCallHistory).toHaveLength(1); expect(stubEngine2.runRulesCallHistory[0].ruleNames).toEqual(expectedStubEngine2RuleNames); expectEquivalentRunOptions(stubEngine2.runRulesCallHistory[0].runOptions, expectedEngineRunOptionsEngine2); expect(fakeFileSystemHandler.dirWasCreated(expectedEngineRunOptionsEngine2.workingFolder)).toEqual(true); - expect(fakeFileSystemHandler.dirWasDeleted(expectedEngineRunOptionsEngine2.workingFolder)).toEqual(true); }); it("When the workspace provided is one that is not constructed from CodeAnalyzer's createWorkspace method, then it should still work", async () => { diff --git a/packages/code-analyzer-core/test/output-format.test.ts b/packages/code-analyzer-core/test/output-format.test.ts index f579ca95..fd780d76 100644 --- a/packages/code-analyzer-core/test/output-format.test.ts +++ b/packages/code-analyzer-core/test/output-format.test.ts @@ -5,7 +5,7 @@ import { RunResults, RunResultsImpl } from "../src/results"; import { RuleImpl, RuleSelection, RuleSelectionImpl } from "../src/rules"; import * as stubs from "./stubs"; import { FixedClock } from "@salesforce/code-analyzer-engine-api/utils"; -import { changeWorkingDirectoryToPackageRoot } from "./test-helpers"; +import {changeWorkingDirectoryToPackageRoot, FakeFileSystemHandler} from "./test-helpers"; import {SeverityLevel} from "@salesforce/code-analyzer-engine-api"; changeWorkingDirectoryToPackageRoot(); @@ -19,6 +19,7 @@ beforeAll(async () => { fixedTime = new Date(2024, 6, 3, 9, 14, 34, 567); codeAnalyzer._setClock(new FixedClock(fixedTime)); const stubPlugin: stubs.StubEnginePlugin = new stubs.StubEnginePlugin(); + codeAnalyzer._setFileSystemHandler(new FakeFileSystemHandler()); await codeAnalyzer.addEnginePlugin(stubPlugin); (stubPlugin.getCreatedEngine('stubEngine1') as stubs.StubEngine1).resultsToReturn = { violations: [ @@ -269,6 +270,7 @@ function getContentsOfExpectedOutputFile(expectedOutputFileName: string, escapeB async function createResultsWithUnexpectedError(): Promise { const codeAnalyzer: CodeAnalyzer = new CodeAnalyzer(CodeAnalyzerConfig.withDefaults()); codeAnalyzer._setClock(new FixedClock(fixedTime)); + codeAnalyzer._setFileSystemHandler(new FakeFileSystemHandler()); await codeAnalyzer.addEnginePlugin(new stubs.ThrowingEnginePlugin()); return codeAnalyzer.run(await codeAnalyzer.selectRules([]), {workspace: await codeAnalyzer.createWorkspace(['test'])}); } @@ -276,6 +278,7 @@ async function createResultsWithUnexpectedError(): Promise { async function createRulesWithEmptyTags(): Promise { const codeAnalyzer: CodeAnalyzer = new CodeAnalyzer(CodeAnalyzerConfig.withDefaults()); codeAnalyzer._setClock(new FixedClock(fixedTime)); + codeAnalyzer._setFileSystemHandler(new FakeFileSystemHandler()); await codeAnalyzer.addEnginePlugin(new stubs.EmptyTagEnginePlugin()); return codeAnalyzer.selectRules(['all']) } diff --git a/packages/code-analyzer-core/test/rule-selection.test.ts b/packages/code-analyzer-core/test/rule-selection.test.ts index f2fdc670..71e724ec 100644 --- a/packages/code-analyzer-core/test/rule-selection.test.ts +++ b/packages/code-analyzer-core/test/rule-selection.test.ts @@ -288,15 +288,12 @@ describe('Tests for selecting rules', () => { workspace: undefined }; expect(fakeFileSystemHandler.dirWasCreated(workingDirectoriesRoot)).toEqual(true); - expect(fakeFileSystemHandler.dirWasDeleted(workingDirectoriesRoot)).toEqual(true); const stubEngine1: stubs.StubEngine1 = plugin.getCreatedEngine('stubEngine1') as stubs.StubEngine1; expect(stubEngine1.describeRulesCallHistory).toEqual([{describeOptions: expectedDescribeOptionsEngine1}]); expect(fakeFileSystemHandler.dirWasCreated(expectedDescribeOptionsEngine1.workingFolder)).toEqual(true); - expect(fakeFileSystemHandler.dirWasDeleted(expectedDescribeOptionsEngine1.workingFolder)).toEqual(true); const stubEngine2: stubs.StubEngine2 = plugin.getCreatedEngine('stubEngine2') as stubs.StubEngine2; expect(stubEngine2.describeRulesCallHistory).toEqual([{describeOptions: expectedDescribeOptionsEngine2}]); expect(fakeFileSystemHandler.dirWasCreated(expectedDescribeOptionsEngine2.workingFolder)).toEqual(true); - expect(fakeFileSystemHandler.dirWasDeleted(expectedDescribeOptionsEngine2.workingFolder)).toEqual(true); }); it('When selectRules is provided with SelectOptions, then they are forwarded to the engines', async () => { diff --git a/packages/code-analyzer-core/test/test-helpers.ts b/packages/code-analyzer-core/test/test-helpers.ts index 31446c9f..21c2f047 100644 --- a/packages/code-analyzer-core/test/test-helpers.ts +++ b/packages/code-analyzer-core/test/test-helpers.ts @@ -29,38 +29,21 @@ export class FixedUniqueIdGenerator implements UniqueIdGenerator { } export class FakeFileSystemHandler implements FileSystemHandler { - private fsMap: Map = new Map(); + private fsSet: Set = new Set(); - createDirectory(absolutePath: string): Promise { - if (this.fsMap.has(absolutePath)) { - throw new Error(`TEST ERROR: Path ${absolutePath} was created twice`); - } - this.fsMap.set(absolutePath, { - created: true, - deleted: false - }); - return Promise.resolve(); + directoryExists(absolutePath: string): boolean { + return this.fsSet.has(absolutePath); } - deleteDirectory(absolutePath: string): Promise { - if (!this.fsMap.has(absolutePath)) { - throw new Error(`TEST ERROR: Path ${absolutePath} was deleted without being created`); - } - if (this.fsMap.get(absolutePath)!.deleted) { - throw new Error(`TEST ERROR: Path ${absolutePath} was deleted twice`); + createDirectory(absolutePath: string): Promise { + if (this.fsSet.has(absolutePath)) { + throw new Error(`TEST ERROR: Path ${absolutePath} was created twice`); } - this.fsMap.get(absolutePath)!.deleted = true; + this.fsSet.add(absolutePath); return Promise.resolve(); } dirWasCreated(absolutePath: string): boolean { - return this.fsMap.has(absolutePath) && this.fsMap.get(absolutePath)!.created; - } - - dirWasDeleted(absolutePath: string): boolean { - return this.fsMap.has(absolutePath) && this.fsMap.get(absolutePath)!.deleted; + return this.fsSet.has(absolutePath); } }