Skip to content

Commit 77f3d40

Browse files
NEW: @W-19397375@: Preserve working folders on error
1 parent 1b5d34e commit 77f3d40

File tree

10 files changed

+510
-181
lines changed

10 files changed

+510
-181
lines changed

package-lock.json

Lines changed: 12 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/code-analyzer-core/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,14 @@
2121
"csv-stringify": "^6.6.0",
2222
"js-yaml": "^4.1.0",
2323
"semver": "^7.7.2",
24-
"xmlbuilder": "^15.1.1",
25-
"tmp": "^0.2.5"
24+
"xmlbuilder": "^15.1.1"
2625
},
2726
"devDependencies": {
2827
"@eslint/js": "^9.35.0",
2928
"@types/js-yaml": "^4.0.9",
3029
"@types/jest": "^30.0.0",
3130
"@types/sarif": "^2.1.7",
3231
"@types/semver": "^7.7.1",
33-
"@types/tmp": "^0.2.6",
3432
"cross-env": "^10.0.0",
3533
"eslint": "^9.35.0",
3634
"jest": "^30.1.3",

packages/code-analyzer-core/src/code-analyzer.ts

Lines changed: 81 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import {Clock, RealClock} from '@salesforce/code-analyzer-engine-api/utils';
2525
import {EventEmitter} from "node:events";
2626
import {CodeAnalyzerConfig, ConfigDescription, EngineOverrides, FIELDS, RuleOverride} from "./config";
2727
import {
28-
EngineProgressAggregator, RuntimeTempFolder,
28+
EngineProgressAggregator,
29+
FileSystem,
30+
RealFileSystem,
2931
RuntimeUniqueIdGenerator,
3032
TempFolder,
3133
toAbsolutePath,
@@ -99,8 +101,8 @@ const MINIMUM_SUPPORTED_NODE = 20;
99101
*/
100102
export class CodeAnalyzer {
101103
private readonly config: CodeAnalyzerConfig;
104+
private readonly tempFolder: TempFolder;
102105
private clock: Clock = new RealClock();
103-
private tempFolder: TempFolder = new RuntimeTempFolder();
104106
private uniqueIdGenerator: UniqueIdGenerator = new RuntimeUniqueIdGenerator();
105107
private readonly eventEmitter: EventEmitter = new EventEmitter();
106108
private readonly engines: Map<string, engApi.Engine> = new Map();
@@ -110,9 +112,14 @@ export class CodeAnalyzer {
110112
private readonly rulesCache: Map<string, RuleImpl[]> = new Map();
111113
private readonly engineRuleDiscoveryProgressAggregator: EngineProgressAggregator = new EngineProgressAggregator();
112114

113-
constructor(config: CodeAnalyzerConfig, version: string = process.version) {
114-
this.validateEnvironment(version);
115+
constructor(config: CodeAnalyzerConfig, fileSystem: FileSystem = new RealFileSystem(), nodeVersion: string = process.version) {
116+
this.validateEnvironment(nodeVersion);
115117
this.config = config;
118+
this.tempFolder = new TempFolder(fileSystem);
119+
/* istanbul ignore next */
120+
process.addListener('exit', () => {
121+
this.tempFolder.removeIfNotKept();
122+
});
116123
}
117124

118125
private validateEnvironment(version: string): void {
@@ -131,9 +138,6 @@ export class CodeAnalyzer {
131138
_setUniqueIdGenerator(uniqueIdGenerator: UniqueIdGenerator): void {
132139
this.uniqueIdGenerator = uniqueIdGenerator;
133140
}
134-
_setTempFolder(tempFolder: TempFolder): void {
135-
this.tempFolder = tempFolder;
136-
}
137141

138142
/**
139143
* Convenience method to return the same CodeAnalyzerConfig instance that was provided to the constructor
@@ -306,21 +310,37 @@ export class CodeAnalyzer {
306310
// called a second time before the first call to run hasn't finished. This can occur if someone builds
307311
// up a bunch of RunResults promises and then does a Promise.all on them. Otherwise, the progress events may
308312
// override each other.
309-
const runWorkingFolderName: string = `code-analyzer-run-${this.clock.formatToDateTimeString()}`;
310313

311314
this.emitLogEvent(LogLevel.Debug, getMessage('RunningWithWorkspace', JSON.stringify({
312315
filesAndFolders: runOptions.workspace.getRawFilesAndFolders(),
313316
targets: runOptions.workspace.getRawTargets()
314317
})));
315318

316-
const runPromises: Promise<EngineRunResults>[] = ruleSelection.getEngineNames().map(
317-
async (engineName) => this.runEngineAndValidateResults(engineName, ruleSelection, {
319+
const engApiWorkspace: engApi.Workspace = toEngApiWorkspace(runOptions.workspace);
320+
const runWorkingFolderName: string = `run-${this.clock.formatToDateTimeString()}`;
321+
await this.tempFolder.makeSubfolder(runWorkingFolderName);
322+
323+
const runPromises: Promise<EngineRunResults>[] = ruleSelection.getEngineNames().map(async (engineName) => {
324+
const workingFolder: string = await this.tempFolder.makeSubfolder(runWorkingFolderName, engineName);
325+
const engineRunOptions: engApi.RunOptions = {
318326
logFolder: this.config.getLogFolder(),
319-
workingFolder: await this.tempFolder.createSubfolder(runWorkingFolderName, engineName),
320-
workspace: toEngApiWorkspace(runOptions.workspace)
321-
}));
327+
workingFolder: workingFolder,
328+
workspace: engApiWorkspace
329+
};
330+
const errorCallback: () => void = () => {
331+
if (!this.tempFolder.isKept(runWorkingFolderName, engineName)) {
332+
this.emitLogEvent(LogLevel.Debug, getMessage('EngineWorkingFolderKept', engineName, workingFolder));
333+
this.tempFolder.markToBeKept(runWorkingFolderName, engineName);
334+
}
335+
};
336+
const results: EngineRunResults = await this.runEngineAndValidateResults(engineName, ruleSelection, engineRunOptions, errorCallback);
337+
await this.tempFolder.removeIfNotKept(runWorkingFolderName, engineName); // TODO: NEED TO CHECK IF RUNNING ASYNC GIVES PERF BOOST HERE OR NOT
338+
return results;
339+
});
322340
const engineRunResultsList: EngineRunResults[] = await Promise.all(runPromises);
323341

342+
await this.tempFolder.removeIfNotKept(runWorkingFolderName); // TODO: NEED TO CHECK IF RUNNING ASYNC GIVES PERF BOOST HERE OR NOT
343+
324344
const runResults: RunResultsImpl = new RunResultsImpl(this.clock);
325345
for (const engineRunResults of engineRunResultsList) {
326346
runResults.addEngineRunResults(engineRunResults);
@@ -344,32 +364,62 @@ export class CodeAnalyzer {
344364

345365
private async getAllRules(workspace?: Workspace): Promise<RuleImpl[]> {
346366
const cacheKey: string = workspace ? workspace.getWorkspaceId() : process.cwd();
347-
const describeWorkingFolderName: string = `code-analyzer-describe-${this.clock.formatToDateTimeString()}`;
348367
if (!this.rulesCache.has(cacheKey)) {
349368
this.engineRuleDiscoveryProgressAggregator.reset(this.getEngineNames());
350369
const engApiWorkspace: engApi.Workspace | undefined = workspace ? toEngApiWorkspace(workspace) : undefined;
351-
const rulePromises: Promise<RuleImpl[]>[] = this.getEngineNames().map(async (engineName) =>
352-
this.getAllRulesFor(engineName, {
370+
const rulesWorkingFolderName: string = `rules-${this.clock.formatToDateTimeString()}`;
371+
372+
await this.tempFolder.makeSubfolder(rulesWorkingFolderName);
373+
374+
const rulePromises: Promise<RuleImpl[]>[] = this.getEngineNames().map(async (engineName) => {
375+
const workingFolder: string = await this.tempFolder.makeSubfolder(rulesWorkingFolderName, engineName);
376+
const describeOptions: engApi.DescribeOptions = {
353377
workspace: engApiWorkspace,
354-
workingFolder: await this.tempFolder.createSubfolder(describeWorkingFolderName, engineName),
378+
workingFolder: workingFolder,
355379
logFolder: this.config.getLogFolder()
356-
}));
380+
};
381+
const errorCallback: () => void = () => {
382+
if (!this.tempFolder.isKept(rulesWorkingFolderName, engineName)) {
383+
this.emitLogEvent(LogLevel.Debug, getMessage('EngineWorkingFolderKept', engineName, workingFolder));
384+
this.tempFolder.markToBeKept(rulesWorkingFolderName, engineName);
385+
}
386+
};
387+
const rules: RuleImpl[] = await this.getAllRulesFor(engineName, describeOptions, errorCallback);
388+
await this.tempFolder.removeIfNotKept(rulesWorkingFolderName, engineName); // TODO: NEED TO CHECK IF RUNNING ASYNC GIVES PERF BOOST HERE OR NOT
389+
return rules;
390+
});
391+
357392
this.rulesCache.set(cacheKey, (await Promise.all(rulePromises)).flat());
393+
394+
await this.tempFolder.removeIfNotKept(rulesWorkingFolderName); // TODO: NEED TO CHECK IF RUNNING ASYNC GIVES PERF BOOST HERE OR NOT
358395
}
359396
return this.rulesCache.get(cacheKey)!;
360397
}
361398

362-
private async getAllRulesFor(engineName: string, describeOptions: engApi.DescribeOptions): Promise<RuleImpl[]> {
399+
private async getAllRulesFor(engineName: string, describeOptions: engApi.DescribeOptions, errorCallback: () => void): Promise<RuleImpl[]> {
363400
this.emitLogEvent(LogLevel.Debug, getMessage('GatheringRulesFromEngine', engineName));
401+
const invokeErrorCallbackIfErrorIsLoggedFcn = (event: engApi.LogEvent) => {
402+
if (event.logLevel === engApi.LogLevel.Error) {
403+
errorCallback();
404+
}
405+
};
406+
407+
const engine: engApi.Engine = this.getEngine(engineName);
408+
engine.onEvent(engApi.EventType.LogEvent, invokeErrorCallbackIfErrorIsLoggedFcn);
409+
364410
let ruleDescriptions: engApi.RuleDescription[] = [];
365411
try {
366-
ruleDescriptions = await this.getEngine(engineName).describeRules(describeOptions);
412+
ruleDescriptions = await engine.describeRules(describeOptions);
367413
} catch (err) {
414+
errorCallback();
368415
this.uninstantiableEnginesMap.set(engineName, err as Error);
369416
this.emitLogEvent(LogLevel.Error, getMessage('PluginErrorWhenGettingRules', engineName, (err as Error).message + '\n\n' +
370417
getMessage('InstructionsToIgnoreErrorAndDisableEngine', engineName)));
371418
return [];
419+
} finally {
420+
engine.removeEventListener(engApi.EventType.LogEvent, invokeErrorCallbackIfErrorIsLoggedFcn);
372421
}
422+
373423
this.emitLogEvent(LogLevel.Debug, getMessage('FinishedGatheringRulesFromEngine', ruleDescriptions.length, engineName));
374424

375425
validateRuleDescriptions(ruleDescriptions, engineName);
@@ -385,20 +435,29 @@ export class CodeAnalyzer {
385435
this.emitEvent({type: EventType.RuleSelectionProgressEvent, timestamp: this.clock.now(), percentComplete: aggregatedPerc});
386436
}
387437

388-
private async runEngineAndValidateResults(engineName: string, ruleSelection: RuleSelection, engineRunOptions: engApi.RunOptions): Promise<EngineRunResults> {
438+
private async runEngineAndValidateResults(engineName: string, ruleSelection: RuleSelection, engineRunOptions: engApi.RunOptions, errorCallback: () => void): Promise<EngineRunResults> {
389439
this.emitEvent<EngineRunProgressEvent>({
390440
type: EventType.EngineRunProgressEvent, timestamp: this.clock.now(), engineName: engineName, percentComplete: 0
391441
});
392-
393442
const rulesToRun: string[] = ruleSelection.getRulesFor(engineName).map(r => r.getName());
443+
394444
this.emitLogEvent(LogLevel.Debug, getMessage('RunningEngineWithRules', engineName, JSON.stringify(rulesToRun)));
445+
const invokeErrorCallbackIfErrorIsLoggedFcn = (event: engApi.LogEvent) => {
446+
if (event.logLevel === engApi.LogLevel.Error) {
447+
errorCallback();
448+
}
449+
};
395450
const engine: engApi.Engine = this.getEngine(engineName);
451+
engine.onEvent(engApi.EventType.LogEvent, invokeErrorCallbackIfErrorIsLoggedFcn);
396452

397453
let apiEngineRunResults: engApi.EngineRunResults;
398454
try {
399455
apiEngineRunResults = await engine.runRules(rulesToRun, engineRunOptions);
400456
} catch (error) {
457+
errorCallback();
401458
return new UnexpectedErrorEngineRunResults(engineName, await engine.getEngineVersion(), error as Error);
459+
} finally {
460+
engine.removeEventListener(engApi.EventType.LogEvent, invokeErrorCallbackIfErrorIsLoggedFcn);
402461
}
403462

404463
validateEngineRunResults(engineName, apiEngineRunResults, ruleSelection);

packages/code-analyzer-core/src/messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ const MESSAGE_CATALOG : MessageCatalog = {
193193
EngineReturnedViolationWithCodeLocationWithEndColumnBeforeStartColumnOnSameLine:
194194
`Engine failure. The engine '%s' returned a violation for rule '%s' that contains a code location with the endLine equal to the startLine and the endColumn %d before the startColumn %d.`,
195195

196+
EngineWorkingFolderKept:
197+
`Since the engine '%s' emitted an error, the following temporary working folder will not be removed: %s`
196198
}
197199

198200
/**

0 commit comments

Comments
 (0)