@@ -25,7 +25,9 @@ import {Clock, RealClock} from '@salesforce/code-analyzer-engine-api/utils';
2525import { EventEmitter } from "node:events" ;
2626import { CodeAnalyzerConfig , ConfigDescription , EngineOverrides , FIELDS , RuleOverride } from "./config" ;
2727import {
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 */
100102export 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,15 @@ 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' , async ( ) => {
121+ // Note that on node exit there is no more event loop, so removal must take place synchronously
122+ this . tempFolder . removeSyncIfNotKept ( ) ;
123+ } ) ;
116124 }
117125
118126 private validateEnvironment ( version : string ) : void {
@@ -131,9 +139,6 @@ export class CodeAnalyzer {
131139 _setUniqueIdGenerator ( uniqueIdGenerator : UniqueIdGenerator ) : void {
132140 this . uniqueIdGenerator = uniqueIdGenerator ;
133141 }
134- _setTempFolder ( tempFolder : TempFolder ) : void {
135- this . tempFolder = tempFolder ;
136- }
137142
138143 /**
139144 * Convenience method to return the same CodeAnalyzerConfig instance that was provided to the constructor
@@ -306,21 +311,37 @@ export class CodeAnalyzer {
306311 // called a second time before the first call to run hasn't finished. This can occur if someone builds
307312 // up a bunch of RunResults promises and then does a Promise.all on them. Otherwise, the progress events may
308313 // override each other.
309- const runWorkingFolderName : string = `code-analyzer-run-${ this . clock . formatToDateTimeString ( ) } ` ;
310314
311315 this . emitLogEvent ( LogLevel . Debug , getMessage ( 'RunningWithWorkspace' , JSON . stringify ( {
312316 filesAndFolders : runOptions . workspace . getRawFilesAndFolders ( ) ,
313317 targets : runOptions . workspace . getRawTargets ( )
314318 } ) ) ) ;
315319
316- const runPromises : Promise < EngineRunResults > [ ] = ruleSelection . getEngineNames ( ) . map (
317- async ( engineName ) => this . runEngineAndValidateResults ( engineName , ruleSelection , {
320+ const engApiWorkspace : engApi . Workspace = toEngApiWorkspace ( runOptions . workspace ) ;
321+ const runWorkingFolderName : string = `run-${ this . clock . formatToDateTimeString ( ) } ` ;
322+ await this . tempFolder . makeSubfolder ( runWorkingFolderName ) ;
323+
324+ const runPromises : Promise < EngineRunResults > [ ] = ruleSelection . getEngineNames ( ) . map ( async ( engineName ) => {
325+ const workingFolder : string = await this . tempFolder . makeSubfolder ( runWorkingFolderName , engineName ) ;
326+ const engineRunOptions : engApi . RunOptions = {
318327 logFolder : this . config . getLogFolder ( ) ,
319- workingFolder : await this . tempFolder . createSubfolder ( runWorkingFolderName , engineName ) ,
320- workspace : toEngApiWorkspace ( runOptions . workspace )
321- } ) ) ;
328+ workingFolder : workingFolder ,
329+ workspace : engApiWorkspace
330+ } ;
331+ const errorCallback : ( ) => void = ( ) => {
332+ if ( ! this . tempFolder . isKept ( runWorkingFolderName , engineName ) ) {
333+ this . emitLogEvent ( LogLevel . Debug , getMessage ( 'EngineWorkingFolderKept' , engineName , workingFolder ) ) ;
334+ this . tempFolder . markToBeKept ( runWorkingFolderName , engineName ) ;
335+ }
336+ } ;
337+ const results : EngineRunResults = await this . runEngineAndValidateResults ( engineName , ruleSelection , engineRunOptions , errorCallback ) ;
338+ await this . tempFolder . removeIfNotKept ( runWorkingFolderName , engineName ) ;
339+ return results ;
340+ } ) ;
322341 const engineRunResultsList : EngineRunResults [ ] = await Promise . all ( runPromises ) ;
323342
343+ await this . tempFolder . removeIfNotKept ( runWorkingFolderName ) ;
344+
324345 const runResults : RunResultsImpl = new RunResultsImpl ( this . clock ) ;
325346 for ( const engineRunResults of engineRunResultsList ) {
326347 runResults . addEngineRunResults ( engineRunResults ) ;
@@ -344,32 +365,62 @@ export class CodeAnalyzer {
344365
345366 private async getAllRules ( workspace ?: Workspace ) : Promise < RuleImpl [ ] > {
346367 const cacheKey : string = workspace ? workspace . getWorkspaceId ( ) : process . cwd ( ) ;
347- const describeWorkingFolderName : string = `code-analyzer-describe-${ this . clock . formatToDateTimeString ( ) } ` ;
348368 if ( ! this . rulesCache . has ( cacheKey ) ) {
349369 this . engineRuleDiscoveryProgressAggregator . reset ( this . getEngineNames ( ) ) ;
350370 const engApiWorkspace : engApi . Workspace | undefined = workspace ? toEngApiWorkspace ( workspace ) : undefined ;
351- const rulePromises : Promise < RuleImpl [ ] > [ ] = this . getEngineNames ( ) . map ( async ( engineName ) =>
352- this . getAllRulesFor ( engineName , {
371+ const rulesWorkingFolderName : string = `rules-${ this . clock . formatToDateTimeString ( ) } ` ;
372+
373+ await this . tempFolder . makeSubfolder ( rulesWorkingFolderName ) ;
374+
375+ const rulePromises : Promise < RuleImpl [ ] > [ ] = this . getEngineNames ( ) . map ( async ( engineName ) => {
376+ const workingFolder : string = await this . tempFolder . makeSubfolder ( rulesWorkingFolderName , engineName ) ;
377+ const describeOptions : engApi . DescribeOptions = {
353378 workspace : engApiWorkspace ,
354- workingFolder : await this . tempFolder . createSubfolder ( describeWorkingFolderName , engineName ) ,
379+ workingFolder : workingFolder ,
355380 logFolder : this . config . getLogFolder ( )
356- } ) ) ;
381+ } ;
382+ const errorCallback : ( ) => void = ( ) => {
383+ if ( ! this . tempFolder . isKept ( rulesWorkingFolderName , engineName ) ) {
384+ this . emitLogEvent ( LogLevel . Debug , getMessage ( 'EngineWorkingFolderKept' , engineName , workingFolder ) ) ;
385+ this . tempFolder . markToBeKept ( rulesWorkingFolderName , engineName ) ;
386+ }
387+ } ;
388+ const rules : RuleImpl [ ] = await this . getAllRulesFor ( engineName , describeOptions , errorCallback ) ;
389+ await this . tempFolder . removeIfNotKept ( rulesWorkingFolderName , engineName ) ;
390+ return rules ;
391+ } ) ;
392+
357393 this . rulesCache . set ( cacheKey , ( await Promise . all ( rulePromises ) ) . flat ( ) ) ;
394+
395+ await this . tempFolder . removeIfNotKept ( rulesWorkingFolderName ) ;
358396 }
359397 return this . rulesCache . get ( cacheKey ) ! ;
360398 }
361399
362- private async getAllRulesFor ( engineName : string , describeOptions : engApi . DescribeOptions ) : Promise < RuleImpl [ ] > {
400+ private async getAllRulesFor ( engineName : string , describeOptions : engApi . DescribeOptions , errorCallback : ( ) => void ) : Promise < RuleImpl [ ] > {
363401 this . emitLogEvent ( LogLevel . Debug , getMessage ( 'GatheringRulesFromEngine' , engineName ) ) ;
402+ const invokeErrorCallbackIfErrorIsLoggedFcn = ( event : engApi . LogEvent ) => {
403+ if ( event . logLevel === engApi . LogLevel . Error ) {
404+ errorCallback ( ) ;
405+ }
406+ } ;
407+
408+ const engine : engApi . Engine = this . getEngine ( engineName ) ;
409+ engine . onEvent ( engApi . EventType . LogEvent , invokeErrorCallbackIfErrorIsLoggedFcn ) ;
410+
364411 let ruleDescriptions : engApi . RuleDescription [ ] = [ ] ;
365412 try {
366- ruleDescriptions = await this . getEngine ( engineName ) . describeRules ( describeOptions ) ;
413+ ruleDescriptions = await engine . describeRules ( describeOptions ) ;
367414 } catch ( err ) {
415+ errorCallback ( ) ;
368416 this . uninstantiableEnginesMap . set ( engineName , err as Error ) ;
369417 this . emitLogEvent ( LogLevel . Error , getMessage ( 'PluginErrorWhenGettingRules' , engineName , ( err as Error ) . message + '\n\n' +
370418 getMessage ( 'InstructionsToIgnoreErrorAndDisableEngine' , engineName ) ) ) ;
371419 return [ ] ;
420+ } finally {
421+ engine . removeEventListener ( engApi . EventType . LogEvent , invokeErrorCallbackIfErrorIsLoggedFcn ) ;
372422 }
423+
373424 this . emitLogEvent ( LogLevel . Debug , getMessage ( 'FinishedGatheringRulesFromEngine' , ruleDescriptions . length , engineName ) ) ;
374425
375426 validateRuleDescriptions ( ruleDescriptions , engineName ) ;
@@ -385,20 +436,29 @@ export class CodeAnalyzer {
385436 this . emitEvent ( { type : EventType . RuleSelectionProgressEvent , timestamp : this . clock . now ( ) , percentComplete : aggregatedPerc } ) ;
386437 }
387438
388- private async runEngineAndValidateResults ( engineName : string , ruleSelection : RuleSelection , engineRunOptions : engApi . RunOptions ) : Promise < EngineRunResults > {
439+ private async runEngineAndValidateResults ( engineName : string , ruleSelection : RuleSelection , engineRunOptions : engApi . RunOptions , errorCallback : ( ) => void ) : Promise < EngineRunResults > {
389440 this . emitEvent < EngineRunProgressEvent > ( {
390441 type : EventType . EngineRunProgressEvent , timestamp : this . clock . now ( ) , engineName : engineName , percentComplete : 0
391442 } ) ;
392-
393443 const rulesToRun : string [ ] = ruleSelection . getRulesFor ( engineName ) . map ( r => r . getName ( ) ) ;
444+
394445 this . emitLogEvent ( LogLevel . Debug , getMessage ( 'RunningEngineWithRules' , engineName , JSON . stringify ( rulesToRun ) ) ) ;
446+ const invokeErrorCallbackIfErrorIsLoggedFcn = ( event : engApi . LogEvent ) => {
447+ if ( event . logLevel === engApi . LogLevel . Error ) {
448+ errorCallback ( ) ;
449+ }
450+ } ;
395451 const engine : engApi . Engine = this . getEngine ( engineName ) ;
452+ engine . onEvent ( engApi . EventType . LogEvent , invokeErrorCallbackIfErrorIsLoggedFcn ) ;
396453
397454 let apiEngineRunResults : engApi . EngineRunResults ;
398455 try {
399456 apiEngineRunResults = await engine . runRules ( rulesToRun , engineRunOptions ) ;
400457 } catch ( error ) {
458+ errorCallback ( ) ;
401459 return new UnexpectedErrorEngineRunResults ( engineName , await engine . getEngineVersion ( ) , error as Error ) ;
460+ } finally {
461+ engine . removeEventListener ( engApi . EventType . LogEvent , invokeErrorCallbackIfErrorIsLoggedFcn ) ;
402462 }
403463
404464 validateEngineRunResults ( engineName , apiEngineRunResults , ruleSelection ) ;
0 commit comments