Skip to content

Commit c94ddbd

Browse files
authored
NEW @W-19397402@ Implemented hidden config property for keeping working d… (#369)
1 parent fda5b84 commit c94ddbd

File tree

8 files changed

+144
-10
lines changed

8 files changed

+144
-10
lines changed

packages/code-analyzer-core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@salesforce/code-analyzer-core",
33
"description": "Core Package for the Salesforce Code Analyzer",
4-
"version": "0.38.1",
4+
"version": "0.39.0-SNAPSHOT",
55
"author": "The Salesforce Code Analyzer Team",
66
"license": "BSD-3-Clause",
77
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
@@ -72,4 +72,4 @@
7272
"!src/index.ts"
7373
]
7474
}
75-
}
75+
}

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,21 +326,28 @@ export class CodeAnalyzer {
326326

327327
const runPromises: Promise<EngineRunResults>[] = ruleSelection.getEngineNames().map(async (engineName) => {
328328
const workingFolder: string = await this.tempFolder.makeSubfolder(runWorkingFolderName, engineName);
329+
if (this.config.getPreserveAllWorkingFolders()) {
330+
this.tempFolder.markToBeKept(runWorkingFolderName, engineName);
331+
}
329332
const engineRunOptions: engApi.RunOptions = {
330333
logFolder: this.config.getLogFolder(),
331334
workingFolder: workingFolder,
332335
workspace: engApiWorkspace
333336
};
334337
const errorCallback: () => void = () => {
338+
// istanbul ignore else
335339
if (!this.tempFolder.isKept(runWorkingFolderName, engineName)) {
336-
this.emitLogEvent(LogLevel.Debug, getMessage('EngineWorkingFolderKept', engineName, workingFolder));
340+
this.emitLogEvent(LogLevel.Debug, getMessage('EngineWorkingFolderKeptDueToError', engineName, workingFolder));
337341
this.tempFolder.markToBeKept(runWorkingFolderName, engineName);
338342
}
339343
};
340344
const results: EngineRunResults = await this.runEngineAndValidateResults(engineName, ruleSelection, engineRunOptions, errorCallback);
341345
await this.tempFolder.removeIfNotKept(runWorkingFolderName, engineName);
342346
return results;
343347
});
348+
if (this.config.getPreserveAllWorkingFolders()) {
349+
this.emitLogEvent(LogLevel.Debug, getMessage('AllWorkingFoldersKept', await this.tempFolder.getPath(runWorkingFolderName)));
350+
}
344351
const engineRunResultsList: EngineRunResults[] = await Promise.all(runPromises);
345352

346353
await this.tempFolder.removeIfNotKept(runWorkingFolderName);
@@ -377,14 +384,19 @@ export class CodeAnalyzer {
377384

378385
const rulePromises: Promise<RuleImpl[]>[] = this.getEngineNames().map(async (engineName) => {
379386
const workingFolder: string = await this.tempFolder.makeSubfolder(rulesWorkingFolderName, engineName);
387+
388+
if (this.config.getPreserveAllWorkingFolders()) {
389+
this.tempFolder.markToBeKept(rulesWorkingFolderName, engineName)
390+
}
391+
380392
const describeOptions: engApi.DescribeOptions = {
381393
workspace: engApiWorkspace,
382394
workingFolder: workingFolder,
383395
logFolder: this.config.getLogFolder()
384396
};
385397
const errorCallback: () => void = () => {
386398
if (!this.tempFolder.isKept(rulesWorkingFolderName, engineName)) {
387-
this.emitLogEvent(LogLevel.Debug, getMessage('EngineWorkingFolderKept', engineName, workingFolder));
399+
this.emitLogEvent(LogLevel.Debug, getMessage('EngineWorkingFolderKeptDueToError', engineName, workingFolder));
388400
this.tempFolder.markToBeKept(rulesWorkingFolderName, engineName);
389401
}
390402
};
@@ -393,6 +405,10 @@ export class CodeAnalyzer {
393405
return rules;
394406
});
395407

408+
if (this.config.getPreserveAllWorkingFolders()) {
409+
this.emitLogEvent(LogLevel.Debug, getMessage('AllWorkingFoldersKept', await this.tempFolder.getPath(rulesWorkingFolderName)));
410+
}
411+
396412
this.rulesCache.set(cacheKey, (await Promise.all(rulePromises)).flat());
397413

398414
await this.tempFolder.removeIfNotKept(rulesWorkingFolderName);

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const FIELDS = {
1414
LOG_FOLDER: 'log_folder',
1515
LOG_LEVEL: 'log_level',
1616
CUSTOM_ENGINE_PLUGIN_MODULES: 'custom_engine_plugin_modules', // Hidden
17+
PRESERVE_ALL_WORKING_FOLDERS: 'preserve_all_working_folders', // Hidden
1718
RULES: 'rules',
1819
ENGINES: 'engines',
1920
SEVERITY: 'severity',
@@ -41,6 +42,7 @@ type TopLevelConfig = {
4142
log_level: LogLevel
4243
rules: Record<string, RuleOverrides>
4344
engines: Record<string, EngineOverrides>
45+
preserve_all_working_folders: boolean // INTERNAL USE ONLY
4446
custom_engine_plugin_modules: string[] // INTERNAL USE ONLY
4547
}
4648

@@ -51,6 +53,7 @@ export const DEFAULT_CONFIG: TopLevelConfig = {
5153
log_level: LogLevel.Debug,
5254
rules: {},
5355
engines: {},
56+
preserve_all_working_folders: false, // INTERNAL USE ONLY
5457
custom_engine_plugin_modules: [], // INTERNAL USE ONLY
5558
};
5659

@@ -136,7 +139,7 @@ export class CodeAnalyzerConfig {
136139
configRoot = !rawConfig.config_root ? (configRoot ?? process.cwd()) :
137140
validateAbsoluteFolder(rawConfig.config_root, FIELDS.CONFIG_ROOT);
138141
const configExtractor: engApi.ConfigValueExtractor = new engApi.ConfigValueExtractor(rawConfig, '', configRoot);
139-
configExtractor.addKeysThatBypassValidation([FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES]); // Because custom_engine_plugin_modules is currently hidden
142+
configExtractor.addKeysThatBypassValidation([FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES, FIELDS.PRESERVE_ALL_WORKING_FOLDERS]); // Hidden fields bypass validation
140143
configExtractor.validateContainsOnlySpecifiedKeys([FIELDS.CONFIG_ROOT, FIELDS.LOG_FOLDER, FIELDS.LOG_LEVEL ,FIELDS.RULES, FIELDS.ENGINES]);
141144
const config: TopLevelConfig = {
142145
config_root: configRoot,
@@ -145,6 +148,7 @@ export class CodeAnalyzerConfig {
145148
custom_engine_plugin_modules: configExtractor.extractArray(FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES,
146149
engApi.ValueValidator.validateString,
147150
DEFAULT_CONFIG.custom_engine_plugin_modules)!,
151+
preserve_all_working_folders: configExtractor.extractBoolean(FIELDS.PRESERVE_ALL_WORKING_FOLDERS, DEFAULT_CONFIG.preserve_all_working_folders)!,
148152
rules: extractRulesValue(configExtractor),
149153
engines: extractEnginesValue(configExtractor)
150154
}
@@ -226,6 +230,15 @@ export class CodeAnalyzerConfig {
226230
return this.config.custom_engine_plugin_modules;
227231
}
228232

233+
/**
234+
* Returns a boolean indicating whether working folders are always preserved.
235+
* If false, then the working folders are deleted unless the engine issues an error.
236+
* If true, then the working folder for each engine remains, even if the engine does not issue an error.
237+
*/
238+
public getPreserveAllWorkingFolders(): boolean {
239+
return this.config.preserve_all_working_folders;
240+
}
241+
229242
/**
230243
* Returns a {@link RuleOverrides} instance containing the user specified overrides for all rules associated with the specified engine
231244
* @param engineName name of the engine

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,10 @@ const MESSAGE_CATALOG : MessageCatalog = {
202202
EngineReturnedViolationWithCodeLocationWithEndColumnBeforeStartColumnOnSameLine:
203203
`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.`,
204204

205-
EngineWorkingFolderKept:
205+
AllWorkingFoldersKept:
206+
`Since preserve_all_working_folders config setting is true, all temporary working folders in %s have been kept.`,
207+
208+
EngineWorkingFolderKeptDueToError:
206209
`Since the engine '%s' emitted an error, the following temporary working folder will not be removed: %s`
207210
}
208211

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

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,14 +201,18 @@ describe("Tests for the run method of CodeAnalyzer", () => {
201201
return codeAnalyzer;
202202
}
203203

204-
beforeEach(async () => {
205-
codeAnalyzer = createCodeAnalyzer();
204+
async function setupCodeAnalyzerWithStubs(config: CodeAnalyzerConfig = CodeAnalyzerConfig.withDefaults()): Promise<void> {
205+
codeAnalyzer = createCodeAnalyzer(config);
206206
sampleRunOptions = {workspace: await codeAnalyzer.createWorkspace([__dirname])};
207207
const stubPlugin: stubs.StubEnginePlugin = new stubs.StubEnginePlugin();
208208
await codeAnalyzer.addEnginePlugin(stubPlugin);
209209
stubEngine1 = stubPlugin.getCreatedEngine('stubEngine1') as stubs.StubEngine1;
210210
stubEngine2 = stubPlugin.getCreatedEngine('stubEngine2') as stubs.StubEngine2;
211211
selection = await codeAnalyzer.selectRules([]);
212+
}
213+
214+
beforeEach(async () => {
215+
await setupCodeAnalyzerWithStubs();
212216
});
213217

214218
it("When run options contains workspace with targets, then they are passed to each engine successfully", async () => {
@@ -861,6 +865,49 @@ describe("Tests for the run method of CodeAnalyzer", () => {
861865
expect(fileSystem.files).not.toContain(expectedRunWorkingFolderForStubEngine3);
862866
});
863867

868+
869+
it("When running rules, if the top-level preserve_all_working_folders flag is true, all run working folders are preserved and a log is issued", async () => {
870+
await setupCodeAnalyzerWithStubs(CodeAnalyzerConfig.fromObject({
871+
preserve_all_working_folders: true
872+
}));
873+
874+
const logEvents: LogEvent[] = [];
875+
codeAnalyzer.onEvent(EventType.LogEvent, (event: LogEvent) => logEvents.push(event));
876+
877+
await codeAnalyzer.run(selection, sampleRunOptions);
878+
879+
const expectedRunWorkingFolderRoot: string = path.join(os.tmpdir(),'code-analyzer-0','run-' + clock.formatToDateTimeString());
880+
const expectedRunWorkingFolderForStubEngine1: string = path.join(expectedRunWorkingFolderRoot, 'stubEngine1');
881+
const expectedRunWorkingFolderForStubEngine2: string = path.join(expectedRunWorkingFolderRoot, 'stubEngine2');
882+
const expectedRunWorkingFolderForStubEngine3: string = path.join(expectedRunWorkingFolderRoot, 'stubEngine3');
883+
884+
// First confirm that the root folder and all 3 engines run working folders were created
885+
const createdFolders: string[] = fileSystem.mkdirCallHistory.map(args => args.absPath.toString());
886+
expect(createdFolders).toContain(expectedRunWorkingFolderRoot);
887+
expect(createdFolders).toContain(expectedRunWorkingFolderForStubEngine1);
888+
expect(createdFolders).toContain(expectedRunWorkingFolderForStubEngine2);
889+
expect(createdFolders).toContain(expectedRunWorkingFolderForStubEngine3);
890+
891+
// Confirm that the root folder and all 3 engines run working folders were removed (because none of them errored during run)
892+
const removedFolders: string[] = fileSystem.rmCallHistory.map(args => args.absPath.toString());
893+
expect(removedFolders).not.toContain(expectedRunWorkingFolderRoot);
894+
expect(removedFolders).not.toContain(expectedRunWorkingFolderForStubEngine1);
895+
expect(removedFolders).not.toContain(expectedRunWorkingFolderForStubEngine2);
896+
expect(removedFolders).not.toContain(expectedRunWorkingFolderForStubEngine3);
897+
898+
// Verify end result
899+
expect(fileSystem.files).toContain(expectedRunWorkingFolderRoot);
900+
expect(fileSystem.files).toContain(expectedRunWorkingFolderForStubEngine1);
901+
expect(fileSystem.files).toContain(expectedRunWorkingFolderForStubEngine2);
902+
expect(fileSystem.files).toContain(expectedRunWorkingFolderForStubEngine3);
903+
904+
// Verify log lines
905+
const relevantLogMsgs: string[] = logEvents.filter(e => e.logLevel === LogLevel.Debug &&
906+
e.message.includes('all temporary working folders in')).map(e => e.message);
907+
908+
expect(relevantLogMsgs.filter(m => m.includes(expectedRunWorkingFolderRoot))).toHaveLength(1);
909+
})
910+
864911
it("When running rules, if an engine issues an error, then we preserve that run working folder and issue a log pointing to it", async () => {
865912
codeAnalyzer = createCodeAnalyzer();
866913
const logEvents: LogEvent[] = [];

packages/code-analyzer-core/test/config.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe("Tests for creating and accessing configuration values", () => {
1919
expect(conf.getLogFolder()).toEqual(os.tmpdir());
2020
expect(conf.getLogLevel()).toEqual(LogLevel.Debug);
2121
expect(conf.getCustomEnginePluginModules()).toEqual([]);
22+
expect(conf.getPreserveAllWorkingFolders()).toEqual(false);
2223
expect(conf.getRuleOverridesFor("stubEngine1")).toEqual({});
2324
expect(conf.getEngineOverridesFor("stubEngine1")).toEqual({});
2425
expect(conf.getRuleOverridesFor("stubEngine2")).toEqual({});
@@ -81,6 +82,7 @@ describe("Tests for creating and accessing configuration values", () => {
8182
const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromFile(path.join(TEST_DATA_DIR, 'sample-config-02.Yml'));
8283
expect(conf.getLogFolder()).toEqual(os.tmpdir());
8384
expect(conf.getCustomEnginePluginModules()).toEqual(['dummy_plugin_module_path']);
85+
expect(conf.getPreserveAllWorkingFolders()).toEqual(true);
8486
expect(conf.getRuleOverridesFor('stubEngine1')).toEqual({});
8587
expect(conf.getRuleOverridesFor('stubEngine2')).toEqual({
8688
stub2RuleC: {
@@ -103,6 +105,7 @@ describe("Tests for creating and accessing configuration values", () => {
103105
const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromFile(path.join(TEST_DATA_DIR, 'sample-config-03.json'));
104106
expect(conf.getLogFolder()).toEqual(path.join(TEST_DATA_DIR, 'sampleLogFolder'));
105107
expect(conf.getCustomEnginePluginModules()).toEqual([]);
108+
expect(conf.getPreserveAllWorkingFolders()).toEqual(false);
106109
expect(conf.getRuleOverridesFor('stubEngine1')).toEqual({});
107110
expect(conf.getRuleOverridesFor('stubEngine2')).toEqual({});
108111
expect(conf.getEngineOverridesFor('stubEngine1')).toEqual({});
@@ -292,6 +295,17 @@ describe("Tests for creating and accessing configuration values", () => {
292295
getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigValueMustBeOfType','custom_engine_plugin_modules', 'array', 'string'));
293296
});
294297

298+
it("When preserve_all_working_folders is not a boolean, then throw an error", () => {
299+
expect(() => CodeAnalyzerConfig.fromObject({preserve_all_working_folders: 3})).toThrow(
300+
getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigValueMustBeOfType','preserve_all_working_folders', 'boolean', 'number'));
301+
302+
expect(() => CodeAnalyzerConfig.fromObject({preserve_all_working_folders: 'abcd'})).toThrow(
303+
getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigValueMustBeOfType','preserve_all_working_folders', 'boolean', 'string'));
304+
305+
expect(() => CodeAnalyzerConfig.fromObject({preserve_all_working_folders: 'true'})).toThrow(
306+
getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigValueMustBeOfType','preserve_all_working_folders', 'boolean', 'string'));
307+
})
308+
295309
it("When supplied config_root path is a valid absolute path, then we use it", () => {
296310
const configRootValue: string = path.join(TEST_DATA_DIR, 'sampleWorkspace');
297311
const conf: CodeAnalyzerConfig = CodeAnalyzerConfig.fromObject({config_root: configRootValue});

packages/code-analyzer-core/test/rule-selection.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,47 @@ describe('Tests for selecting rules', () => {
641641
expect(relevantLogMsgs.filter(m => m.endsWith(expectedRulesWorkingFolderForStubEngine2))).toHaveLength(1);
642642
});
643643

644+
it("When selecting rules, if preserve_all_working_folders is true, then all working folders are kept regardless of failures and the root is logged", async () => {
645+
await setupCodeAnalyzerWithStubPlugin(CodeAnalyzerConfig.fromObject({
646+
preserve_all_working_folders: true
647+
}));
648+
649+
const logEvents: LogEvent[] = [];
650+
codeAnalyzer.onEvent(EventType.LogEvent, (event: LogEvent) => logEvents.push(event));
651+
652+
await codeAnalyzer.selectRules(['all']);
653+
654+
const expectedRulesWorkingFolderRoot: string = path.join(os.tmpdir(),'code-analyzer-0','rules-' + clock.formatToDateTimeString());
655+
const expectedRulesWorkingFolderForStubEngine1: string = path.join(expectedRulesWorkingFolderRoot, 'stubEngine1');
656+
const expectedRulesWorkingFolderForStubEngine2: string = path.join(expectedRulesWorkingFolderRoot, 'stubEngine2');
657+
const expectedRulesWorkingFolderForStubEngine3: string = path.join(expectedRulesWorkingFolderRoot, 'stubEngine3');
658+
659+
// First confirm that the root folder and all 3 engine rule working folders were created
660+
const createdFolders: string[] = fileSystem.mkdirCallHistory.map(args => args.absPath.toString());
661+
expect(createdFolders).toContain(expectedRulesWorkingFolderRoot);
662+
expect(createdFolders).toContain(expectedRulesWorkingFolderForStubEngine1);
663+
expect(createdFolders).toContain(expectedRulesWorkingFolderForStubEngine2);
664+
expect(createdFolders).toContain(expectedRulesWorkingFolderForStubEngine3);
665+
666+
// Next confirm that neither the root folder nor the subfolders were removed.
667+
const removedFolders: string[] = fileSystem.rmCallHistory.map(args => args.absPath.toString());
668+
expect(removedFolders).not.toContain(expectedRulesWorkingFolderRoot);
669+
expect(removedFolders).not.toContain(expectedRulesWorkingFolderForStubEngine1);
670+
expect(removedFolders).not.toContain(expectedRulesWorkingFolderForStubEngine2);
671+
expect(removedFolders).not.toContain(expectedRulesWorkingFolderForStubEngine3);
672+
673+
// Verify end result
674+
expect(fileSystem.files).toContain(expectedRulesWorkingFolderRoot);
675+
expect(fileSystem.files).toContain(expectedRulesWorkingFolderForStubEngine1);
676+
expect(fileSystem.files).toContain(expectedRulesWorkingFolderForStubEngine2);
677+
expect(fileSystem.files).toContain(expectedRulesWorkingFolderForStubEngine3);
678+
679+
// Verify log lines
680+
const relevantLogMsgs: string[] = logEvents.filter(e => e.logLevel === LogLevel.Debug &&
681+
e.message.includes('all temporary working folders in')).map(e => e.message);
682+
expect(relevantLogMsgs.filter(m => m.includes(expectedRulesWorkingFolderRoot))).toHaveLength(1);
683+
});
684+
644685
it("When selecting rules, if no engine errors, then we fully remove the rules working folder", async () => {
645686
codeAnalyzer = createCodeAnalyzer();
646687
await codeAnalyzer.addEnginePlugin(new stubs.EmptyTagEnginePlugin());

packages/code-analyzer-core/test/test-data/sample-config-02.Yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
custom_engine_plugin_modules: ["dummy_plugin_module_path"]
2-
2+
preserve_all_working_folders: true
33
rules:
44
stubEngine2:
55
stub2RuleC:
@@ -10,4 +10,4 @@ engines:
1010
miscSetting1: true
1111
miscSetting2:
1212
miscSetting2A: 3
13-
miscSetting2B: ["hello", "world"]
13+
miscSetting2B: ["hello", "world"]

0 commit comments

Comments
 (0)