-
Notifications
You must be signed in to change notification settings - Fork 4
NEW: @W-19397375@: Preserve working folders on error #355
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -25,7 +25,9 @@ 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, RuntimeTempFolder, | ||||||
| EngineProgressAggregator, | ||||||
| FileSystem, | ||||||
| RealFileSystem, | ||||||
| RuntimeUniqueIdGenerator, | ||||||
| TempFolder, | ||||||
| toAbsolutePath, | ||||||
|
|
@@ -99,8 +101,8 @@ const MINIMUM_SUPPORTED_NODE = 20; | |||||
| */ | ||||||
| export class CodeAnalyzer { | ||||||
| private readonly config: CodeAnalyzerConfig; | ||||||
| private readonly tempFolder: TempFolder; | ||||||
| private clock: Clock = new RealClock(); | ||||||
| private tempFolder: TempFolder = new RuntimeTempFolder(); | ||||||
| private uniqueIdGenerator: UniqueIdGenerator = new RuntimeUniqueIdGenerator(); | ||||||
| private readonly eventEmitter: EventEmitter = new EventEmitter(); | ||||||
| private readonly engines: Map<string, engApi.Engine> = new Map(); | ||||||
|
|
@@ -110,9 +112,15 @@ export class CodeAnalyzer { | |||||
| private readonly rulesCache: Map<string, RuleImpl[]> = new Map(); | ||||||
| private readonly engineRuleDiscoveryProgressAggregator: EngineProgressAggregator = new EngineProgressAggregator(); | ||||||
|
|
||||||
| constructor(config: CodeAnalyzerConfig, version: string = process.version) { | ||||||
| this.validateEnvironment(version); | ||||||
| constructor(config: CodeAnalyzerConfig, fileSystem: FileSystem = new RealFileSystem(), nodeVersion: string = process.version) { | ||||||
| this.validateEnvironment(nodeVersion); | ||||||
| this.config = config; | ||||||
| this.tempFolder = new TempFolder(fileSystem); | ||||||
| /* istanbul ignore next */ | ||||||
| process.addListener('exit', async () => { | ||||||
| // Note that on node exit there is no more event loop, so removal must take place synchronously | ||||||
| this.tempFolder.removeSyncIfNotKept(); | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| private validateEnvironment(version: string): void { | ||||||
|
|
@@ -131,9 +139,6 @@ export class CodeAnalyzer { | |||||
| _setUniqueIdGenerator(uniqueIdGenerator: UniqueIdGenerator): void { | ||||||
| this.uniqueIdGenerator = uniqueIdGenerator; | ||||||
| } | ||||||
| _setTempFolder(tempFolder: TempFolder): void { | ||||||
| this.tempFolder = tempFolder; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Convenience method to return the same CodeAnalyzerConfig instance that was provided to the constructor | ||||||
|
|
@@ -306,21 +311,37 @@ 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 runWorkingFolderName: string = `code-analyzer-run-${this.clock.formatToDateTimeString()}`; | ||||||
|
|
||||||
| this.emitLogEvent(LogLevel.Debug, getMessage('RunningWithWorkspace', JSON.stringify({ | ||||||
| filesAndFolders: runOptions.workspace.getRawFilesAndFolders(), | ||||||
| targets: runOptions.workspace.getRawTargets() | ||||||
| }))); | ||||||
|
|
||||||
| const runPromises: Promise<EngineRunResults>[] = ruleSelection.getEngineNames().map( | ||||||
| async (engineName) => this.runEngineAndValidateResults(engineName, ruleSelection, { | ||||||
| const engApiWorkspace: engApi.Workspace = toEngApiWorkspace(runOptions.workspace); | ||||||
| const runWorkingFolderName: string = `run-${this.clock.formatToDateTimeString()}`; | ||||||
| await this.tempFolder.makeSubfolder(runWorkingFolderName); | ||||||
|
|
||||||
| const runPromises: Promise<EngineRunResults>[] = ruleSelection.getEngineNames().map(async (engineName) => { | ||||||
| const workingFolder: string = await this.tempFolder.makeSubfolder(runWorkingFolderName, engineName); | ||||||
| const engineRunOptions: engApi.RunOptions = { | ||||||
| logFolder: this.config.getLogFolder(), | ||||||
| workingFolder: await this.tempFolder.createSubfolder(runWorkingFolderName, engineName), | ||||||
| workspace: toEngApiWorkspace(runOptions.workspace) | ||||||
| })); | ||||||
| workingFolder: workingFolder, | ||||||
| workspace: engApiWorkspace | ||||||
| }; | ||||||
| const errorCallback: () => void = () => { | ||||||
| if (!this.tempFolder.isKept(runWorkingFolderName, engineName)) { | ||||||
| this.emitLogEvent(LogLevel.Debug, getMessage('EngineWorkingFolderKept', engineName, workingFolder)); | ||||||
| this.tempFolder.markToBeKept(runWorkingFolderName, engineName); | ||||||
| } | ||||||
| }; | ||||||
| const results: EngineRunResults = await this.runEngineAndValidateResults(engineName, ruleSelection, engineRunOptions, errorCallback); | ||||||
| await this.tempFolder.removeIfNotKept(runWorkingFolderName, engineName); | ||||||
| return results; | ||||||
| }); | ||||||
| const engineRunResultsList: EngineRunResults[] = await Promise.all(runPromises); | ||||||
|
|
||||||
| await this.tempFolder.removeIfNotKept(runWorkingFolderName); | ||||||
|
|
||||||
| const runResults: RunResultsImpl = new RunResultsImpl(this.clock); | ||||||
| for (const engineRunResults of engineRunResultsList) { | ||||||
| runResults.addEngineRunResults(engineRunResults); | ||||||
|
|
@@ -344,32 +365,62 @@ export class CodeAnalyzer { | |||||
|
|
||||||
| private async getAllRules(workspace?: Workspace): Promise<RuleImpl[]> { | ||||||
| const cacheKey: string = workspace ? workspace.getWorkspaceId() : process.cwd(); | ||||||
| const describeWorkingFolderName: string = `code-analyzer-describe-${this.clock.formatToDateTimeString()}`; | ||||||
| if (!this.rulesCache.has(cacheKey)) { | ||||||
| this.engineRuleDiscoveryProgressAggregator.reset(this.getEngineNames()); | ||||||
| const engApiWorkspace: engApi.Workspace | undefined = workspace ? toEngApiWorkspace(workspace) : undefined; | ||||||
| const rulePromises: Promise<RuleImpl[]>[] = this.getEngineNames().map(async (engineName) => | ||||||
| this.getAllRulesFor(engineName, { | ||||||
| const rulesWorkingFolderName: string = `rules-${this.clock.formatToDateTimeString()}`; | ||||||
|
|
||||||
| await this.tempFolder.makeSubfolder(rulesWorkingFolderName); | ||||||
|
|
||||||
| const rulePromises: Promise<RuleImpl[]>[] = this.getEngineNames().map(async (engineName) => { | ||||||
| const workingFolder: string = await this.tempFolder.makeSubfolder(rulesWorkingFolderName, engineName); | ||||||
| const describeOptions: engApi.DescribeOptions = { | ||||||
| workspace: engApiWorkspace, | ||||||
| workingFolder: await this.tempFolder.createSubfolder(describeWorkingFolderName, engineName), | ||||||
| workingFolder: workingFolder, | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice cleanup 😎
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually find that this syntax is super confusing and it ties the variable name to the field name. So I personally choose not to use this type of syntax from javascript typically. Just a personal preference.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For example, if I use my editor to rename a variable - it causes issues if I have "workingFolder" as both the fieldname and the variable. I might not want to change the field name... but may want to change the variable name and editors don't know this typically and get it wrong. Sometimes convenience and short code causes bugs... which is why I love static typing as well. :-)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Works for me! |
||||||
| logFolder: this.config.getLogFolder() | ||||||
| })); | ||||||
| }; | ||||||
| const errorCallback: () => void = () => { | ||||||
| if (!this.tempFolder.isKept(rulesWorkingFolderName, engineName)) { | ||||||
| this.emitLogEvent(LogLevel.Debug, getMessage('EngineWorkingFolderKept', engineName, workingFolder)); | ||||||
| this.tempFolder.markToBeKept(rulesWorkingFolderName, engineName); | ||||||
| } | ||||||
| }; | ||||||
| const rules: RuleImpl[] = await this.getAllRulesFor(engineName, describeOptions, errorCallback); | ||||||
| await this.tempFolder.removeIfNotKept(rulesWorkingFolderName, engineName); | ||||||
| return rules; | ||||||
| }); | ||||||
|
|
||||||
| this.rulesCache.set(cacheKey, (await Promise.all(rulePromises)).flat()); | ||||||
|
|
||||||
| await this.tempFolder.removeIfNotKept(rulesWorkingFolderName); | ||||||
| } | ||||||
| return this.rulesCache.get(cacheKey)!; | ||||||
| } | ||||||
|
|
||||||
| private async getAllRulesFor(engineName: string, describeOptions: engApi.DescribeOptions): Promise<RuleImpl[]> { | ||||||
| private async getAllRulesFor(engineName: string, describeOptions: engApi.DescribeOptions, errorCallback: () => void): Promise<RuleImpl[]> { | ||||||
| this.emitLogEvent(LogLevel.Debug, getMessage('GatheringRulesFromEngine', engineName)); | ||||||
| const invokeErrorCallbackIfErrorIsLoggedFcn = (event: engApi.LogEvent) => { | ||||||
| if (event.logLevel === engApi.LogLevel.Error) { | ||||||
| errorCallback(); | ||||||
| } | ||||||
| }; | ||||||
|
|
||||||
| const engine: engApi.Engine = this.getEngine(engineName); | ||||||
| engine.onEvent(engApi.EventType.LogEvent, invokeErrorCallbackIfErrorIsLoggedFcn); | ||||||
|
|
||||||
| let ruleDescriptions: engApi.RuleDescription[] = []; | ||||||
| try { | ||||||
| ruleDescriptions = await this.getEngine(engineName).describeRules(describeOptions); | ||||||
| ruleDescriptions = await engine.describeRules(describeOptions); | ||||||
| } catch (err) { | ||||||
| errorCallback(); | ||||||
| this.uninstantiableEnginesMap.set(engineName, err as Error); | ||||||
| this.emitLogEvent(LogLevel.Error, getMessage('PluginErrorWhenGettingRules', engineName, (err as Error).message + '\n\n' + | ||||||
| getMessage('InstructionsToIgnoreErrorAndDisableEngine', engineName))); | ||||||
| return []; | ||||||
| } finally { | ||||||
| engine.removeEventListener(engApi.EventType.LogEvent, invokeErrorCallbackIfErrorIsLoggedFcn); | ||||||
| } | ||||||
|
|
||||||
| this.emitLogEvent(LogLevel.Debug, getMessage('FinishedGatheringRulesFromEngine', ruleDescriptions.length, engineName)); | ||||||
|
|
||||||
| validateRuleDescriptions(ruleDescriptions, engineName); | ||||||
|
|
@@ -385,20 +436,29 @@ 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<EngineRunResults> { | ||||||
| private async runEngineAndValidateResults(engineName: string, ruleSelection: RuleSelection, engineRunOptions: engApi.RunOptions, errorCallback: () => void): Promise<EngineRunResults> { | ||||||
| this.emitEvent<EngineRunProgressEvent>({ | ||||||
| type: EventType.EngineRunProgressEvent, timestamp: this.clock.now(), engineName: engineName, percentComplete: 0 | ||||||
| }); | ||||||
|
|
||||||
| const rulesToRun: string[] = ruleSelection.getRulesFor(engineName).map(r => r.getName()); | ||||||
|
|
||||||
| this.emitLogEvent(LogLevel.Debug, getMessage('RunningEngineWithRules', engineName, JSON.stringify(rulesToRun))); | ||||||
| const invokeErrorCallbackIfErrorIsLoggedFcn = (event: engApi.LogEvent) => { | ||||||
| if (event.logLevel === engApi.LogLevel.Error) { | ||||||
| errorCallback(); | ||||||
| } | ||||||
| }; | ||||||
| const engine: engApi.Engine = this.getEngine(engineName); | ||||||
| engine.onEvent(engApi.EventType.LogEvent, invokeErrorCallbackIfErrorIsLoggedFcn); | ||||||
|
|
||||||
| let apiEngineRunResults: engApi.EngineRunResults; | ||||||
| try { | ||||||
| apiEngineRunResults = await engine.runRules(rulesToRun, engineRunOptions); | ||||||
| } catch (error) { | ||||||
| errorCallback(); | ||||||
| return new UnexpectedErrorEngineRunResults(engineName, await engine.getEngineVersion(), error as Error); | ||||||
| } finally { | ||||||
| engine.removeEventListener(engApi.EventType.LogEvent, invokeErrorCallbackIfErrorIsLoggedFcn); | ||||||
| } | ||||||
|
|
||||||
| validateEngineRunResults(engineName, apiEngineRunResults, ruleSelection); | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you clarify why this needed the ignore?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The callback is really what's ignored. The test could never measure this because the node process is on exit when the call back is fired. So no way to cover this with a unit test. Demo (end-to-end) shows things working though.