From 6bcbdada88a8fe09409bcb9c19e5fda39e570f33 Mon Sep 17 00:00:00 2001 From: Joshua Feingold Date: Wed, 2 Apr 2025 13:01:53 -0500 Subject: [PATCH 1/2] @W-17915877@ SFGE properly handles lists of files for projectdir --- .../metainfo/AbstractMetaInfoCollector.java | 29 ++++++-- .../CustomSettingsInfoCollectorTest.java | 7 ++ .../metainfo/VisualForceHandlerImplTest.java | 25 +++++++ .../classes/MyClass.cls | 8 +++ .../objects/My_Cust_Set__c.object-meta.xml | 8 +++ .../objects/Simple_SObject__c.object-meta.xml | 68 +++++++++++++++++++ .../classes/MyController.cls | 5 ++ .../classes/MyNonController.cls | 5 ++ .../pages/MyVfPage.page | 2 + 9 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/CustomSettingsInfoCollectorTest/loadCustomSettings_testWithIndividualApexFiles/classes/MyClass.cls create mode 100644 packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/CustomSettingsInfoCollectorTest/loadCustomSettings_testWithIndividualApexFiles/objects/My_Cust_Set__c.object-meta.xml create mode 100644 packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/CustomSettingsInfoCollectorTest/loadCustomSettings_testWithIndividualApexFiles/objects/Simple_SObject__c.object-meta.xml create mode 100644 packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/VisualForceHandlerImplTest/loadVisualForce_testWithIndividualApexFiles/classes/MyController.cls create mode 100644 packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/VisualForceHandlerImplTest/loadVisualForce_testWithIndividualApexFiles/classes/MyNonController.cls create mode 100644 packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/VisualForceHandlerImplTest/loadVisualForce_testWithIndividualApexFiles/pages/MyVfPage.page diff --git a/packages/code-analyzer-sfge-engine/sfge/src/main/java/com/salesforce/metainfo/AbstractMetaInfoCollector.java b/packages/code-analyzer-sfge-engine/sfge/src/main/java/com/salesforce/metainfo/AbstractMetaInfoCollector.java index b9c28b54..b40f9c0d 100644 --- a/packages/code-analyzer-sfge-engine/sfge/src/main/java/com/salesforce/metainfo/AbstractMetaInfoCollector.java +++ b/packages/code-analyzer-sfge-engine/sfge/src/main/java/com/salesforce/metainfo/AbstractMetaInfoCollector.java @@ -24,10 +24,12 @@ public abstract class AbstractMetaInfoCollector implements MetaInfoCollector { private static final String APEX_FILE_EXTENSION = ".cls"; protected final TreeSet collectedMetaInfo; private final TreeSet acceptedExtensions = getAcceptedExtensions(); + private final TreeSet pathsWalked; private boolean projectFilesLoaded; AbstractMetaInfoCollector() { this.collectedMetaInfo = CollectionUtil.newTreeSet(); + this.pathsWalked = CollectionUtil.newTreeSet(); } /** @@ -61,13 +63,21 @@ public TreeSet getMetaInfoCollected() { private void processSourceFolder(String sourceFolder) throws MetaInfoLoadException { Path path = new File(sourceFolder).toPath(); - // If the directory has any apex files, we should assume it's the class folder and that - // project files are in a sibling. - // So we'll go up a level before walking the file tree. - if (directoryContainsApex(path)) { + if (isDirectoryContainingApex(path)) { + // If the path is a directory with apex files in it, we should assume it's the class folder, and that project + // files are in a sibling. So we'll go up a level before walking the file tree. path = path.getParent(); + } else if (isApexFile(path)) { + // If the path itself is an apex file, we should assume that it is contained in the class folder, and that + // project files are in a sibling of its parent directory. So we'll go up two levels before walking the file + // tree. + path = path.getParent().getParent(); } final ProjectFileVisitor projectFileVisitor = new ProjectFileVisitor(); + if (this.pathsWalked.contains(path.toString())) { + return; + } + this.pathsWalked.add(path.toString()); try { Files.walkFileTree(path, projectFileVisitor); } catch (IOException ex) { @@ -78,7 +88,7 @@ private void processSourceFolder(String sourceFolder) throws MetaInfoLoadExcepti } } - private boolean directoryContainsApex(Path path) { + private boolean isDirectoryContainingApex(Path path) { final File dir = path.toFile(); // Non-directories obviously don't have any apex. if (!dir.isDirectory()) { @@ -95,6 +105,15 @@ private boolean directoryContainsApex(Path path) { return Arrays.stream(dirContents).anyMatch(f -> f.getName().endsWith(APEX_FILE_EXTENSION)); } + private boolean isApexFile(Path path) { + final File file = path.toFile(); + + if (!file.isFile()) { + return false; + } + return file.getName().endsWith(APEX_FILE_EXTENSION); + } + protected boolean pathMatches(Path path) { final String pathExtension = getPathExtension(path); return acceptedExtensions.contains(pathExtension); diff --git a/packages/code-analyzer-sfge-engine/sfge/src/test/java/com/salesforce/metainfo/CustomSettingsInfoCollectorTest.java b/packages/code-analyzer-sfge-engine/sfge/src/test/java/com/salesforce/metainfo/CustomSettingsInfoCollectorTest.java index 8445b9af..6d96e3c1 100644 --- a/packages/code-analyzer-sfge-engine/sfge/src/test/java/com/salesforce/metainfo/CustomSettingsInfoCollectorTest.java +++ b/packages/code-analyzer-sfge-engine/sfge/src/test/java/com/salesforce/metainfo/CustomSettingsInfoCollectorTest.java @@ -11,6 +11,8 @@ import com.salesforce.rules.ApexFlsViolationRule; import com.salesforce.rules.Violation; import com.salesforce.rules.fls.apex.operations.FlsConstants; + +import java.nio.file.Path; import java.util.Collections; import java.util.List; import java.util.Set; @@ -43,6 +45,11 @@ public void loadCustomSettings_testWithRootFolder(TestInfo testInfo) throws Exce verifyBaseDirIsAvailable(testInfo, "root"); } + @Test + public void loadCustomSettings_testWithIndividualApexFiles(TestInfo testInfo) throws Exception { + verifyBaseDirIsAvailable(testInfo, Path.of("classes", "MyClass.cls").toString()); + } + @Test public void testCustomSettingRegistered(TestInfo testInfo) { final MetaInfoCollector metaInfoCollector = diff --git a/packages/code-analyzer-sfge-engine/sfge/src/test/java/com/salesforce/metainfo/VisualForceHandlerImplTest.java b/packages/code-analyzer-sfge-engine/sfge/src/test/java/com/salesforce/metainfo/VisualForceHandlerImplTest.java index fe1b8f85..b1a7d503 100644 --- a/packages/code-analyzer-sfge-engine/sfge/src/test/java/com/salesforce/metainfo/VisualForceHandlerImplTest.java +++ b/packages/code-analyzer-sfge-engine/sfge/src/test/java/com/salesforce/metainfo/VisualForceHandlerImplTest.java @@ -8,6 +8,9 @@ import com.salesforce.TestUtil; import com.salesforce.graph.ops.GraphUtil; + +import java.nio.file.Path; +import java.util.Arrays; import java.util.Collections; import java.util.Set; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; @@ -61,6 +64,28 @@ public void loadVisualForce_testWithClassFolder(TestInfo testInfo) throws Except } } + @Test + public void loadVisualForce_testWithIndividualApexFiles(TestInfo testInfo) throws Exception { + TestUtil.compileTestFiles(g, testInfo); + try { + MetaInfoCollectorTestProvider.setVisualForceHandler(new VisualForceHandlerImpl()); + String[] sourceFiles = new String[]{ + TestUtil.getTestFileDirectory(testInfo).resolve(Path.of("classes", "MyController.cls")).toString(), + TestUtil.getTestFileDirectory(testInfo).resolve(Path.of("classes", "MyNonController.cls")).toString() + }; + MetaInfoCollector metaInfoCollector = MetaInfoCollectorProvider.getVisualForceHandler(); + metaInfoCollector.loadProjectFiles(Arrays.asList(sourceFiles)); + Set referencedNames = metaInfoCollector.getMetaInfoCollected(); + // When provided with a folder that contains apex, that folder and its siblings should + // be scanned for VF + // files. + MatcherAssert.assertThat(referencedNames, hasSize(equalTo(1))); + MatcherAssert.assertThat(referencedNames, contains("MyController")); + } finally { + MetaInfoCollectorTestProvider.removeVisualForceHandler(); + } + } + @Test public void loadVisualForce_testWithMalformedVf(TestInfo testInfo) throws Exception { TestUtil.compileTestFiles(g, testInfo); diff --git a/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/CustomSettingsInfoCollectorTest/loadCustomSettings_testWithIndividualApexFiles/classes/MyClass.cls b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/CustomSettingsInfoCollectorTest/loadCustomSettings_testWithIndividualApexFiles/classes/MyClass.cls new file mode 100644 index 00000000..756c1c2a --- /dev/null +++ b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/CustomSettingsInfoCollectorTest/loadCustomSettings_testWithIndividualApexFiles/classes/MyClass.cls @@ -0,0 +1,8 @@ +public class MyClass { + @AuraEnabled + public static void foo(List toInsert) { + if (My_Custom_Set__c.SObjectType.getDescribe().isCreateable()) { + insert toInsert; + } + } +} diff --git a/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/CustomSettingsInfoCollectorTest/loadCustomSettings_testWithIndividualApexFiles/objects/My_Cust_Set__c.object-meta.xml b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/CustomSettingsInfoCollectorTest/loadCustomSettings_testWithIndividualApexFiles/objects/My_Cust_Set__c.object-meta.xml new file mode 100644 index 00000000..aa5fb7df --- /dev/null +++ b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/CustomSettingsInfoCollectorTest/loadCustomSettings_testWithIndividualApexFiles/objects/My_Cust_Set__c.object-meta.xml @@ -0,0 +1,8 @@ + + + List + Dummy Custom settings + false + + Public + diff --git a/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/CustomSettingsInfoCollectorTest/loadCustomSettings_testWithIndividualApexFiles/objects/Simple_SObject__c.object-meta.xml b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/CustomSettingsInfoCollectorTest/loadCustomSettings_testWithIndividualApexFiles/objects/Simple_SObject__c.object-meta.xml new file mode 100644 index 00000000..1a14046c --- /dev/null +++ b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/CustomSettingsInfoCollectorTest/loadCustomSettings_testWithIndividualApexFiles/objects/Simple_SObject__c.object-meta.xml @@ -0,0 +1,68 @@ + + + + Accept + Default + + + CancelEdit + Default + + + Clone + Default + + + Delete + Default + + + Edit + Default + + + Follow + Default + + + List + Default + + + New + Default + + + SaveEdit + Default + + + Tab + Default + + + View + Default + + false + Deployed + Simple sobject to test + false + true + false + true + true + true + true + true + + + N-{00} + + false + AutoNumber + + Simple SObjects + + ReadWrite + diff --git a/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/VisualForceHandlerImplTest/loadVisualForce_testWithIndividualApexFiles/classes/MyController.cls b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/VisualForceHandlerImplTest/loadVisualForce_testWithIndividualApexFiles/classes/MyController.cls new file mode 100644 index 00000000..d6f091d4 --- /dev/null +++ b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/VisualForceHandlerImplTest/loadVisualForce_testWithIndividualApexFiles/classes/MyController.cls @@ -0,0 +1,5 @@ +public class MyController { + public String someStringMethod() { + return 'beep'; + } +} diff --git a/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/VisualForceHandlerImplTest/loadVisualForce_testWithIndividualApexFiles/classes/MyNonController.cls b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/VisualForceHandlerImplTest/loadVisualForce_testWithIndividualApexFiles/classes/MyNonController.cls new file mode 100644 index 00000000..6ad5e222 --- /dev/null +++ b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/VisualForceHandlerImplTest/loadVisualForce_testWithIndividualApexFiles/classes/MyNonController.cls @@ -0,0 +1,5 @@ +public class MyNonController { + public String someMethod() { + return 'beep'; + } +} diff --git a/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/VisualForceHandlerImplTest/loadVisualForce_testWithIndividualApexFiles/pages/MyVfPage.page b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/VisualForceHandlerImplTest/loadVisualForce_testWithIndividualApexFiles/pages/MyVfPage.page new file mode 100644 index 00000000..972bade9 --- /dev/null +++ b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/metainfo/VisualForceHandlerImplTest/loadVisualForce_testWithIndividualApexFiles/pages/MyVfPage.page @@ -0,0 +1,2 @@ + + From 96105bc4197471dfc428b43929004b98c189ff0b Mon Sep 17 00:00:00 2001 From: Joshua Feingold Date: Thu, 3 Apr 2025 10:00:30 -0500 Subject: [PATCH 2/2] @W-17915877@ Implemented path start points for SFGE --- .../code-analyzer-sfge-engine/src/engine.ts | 7 +-- .../src/sfge-wrapper.ts | 35 ++++++++---- .../test/engine.test.ts | 55 ++++++++++++++++--- ...FlsViolationRule_violations.goldfile.json} | 0 .../all_violations.goldfile.json} | 0 ...entryPointMethod1_violations.goldfile.json | 21 +++++++ .../EntryClass1_violations.goldfile.json | 38 +++++++++++++ .../sampleRelevantWorkspace2/EntryClass1.cls | 11 ++++ .../sampleRelevantWorkspace2/EntryClass2.cls | 11 ++++ .../InternalOnlyClass.cls | 7 +++ 10 files changed, 164 insertions(+), 21 deletions(-) rename packages/code-analyzer-sfge-engine/test/test-data/goldfiles/{ApexFlsViolationRule_sampleRelevantWorkspace_violations.goldfile.json => sampleRelevantWorkspace/ApexFlsViolationRule_violations.goldfile.json} (100%) rename packages/code-analyzer-sfge-engine/test/test-data/goldfiles/{all_sampleRelevantWorkspace_violations.goldfile.json => sampleRelevantWorkspace/all_violations.goldfile.json} (100%) create mode 100644 packages/code-analyzer-sfge-engine/test/test-data/goldfiles/sampleRelevantWorkspace2/EntryClass1_entryPointMethod1_violations.goldfile.json create mode 100644 packages/code-analyzer-sfge-engine/test/test-data/goldfiles/sampleRelevantWorkspace2/EntryClass1_violations.goldfile.json create mode 100644 packages/code-analyzer-sfge-engine/test/test-data/sampleRelevantWorkspace2/EntryClass1.cls create mode 100644 packages/code-analyzer-sfge-engine/test/test-data/sampleRelevantWorkspace2/EntryClass2.cls create mode 100644 packages/code-analyzer-sfge-engine/test/test-data/sampleRelevantWorkspace2/InternalOnlyClass.cls diff --git a/packages/code-analyzer-sfge-engine/src/engine.ts b/packages/code-analyzer-sfge-engine/src/engine.ts index 472e90fe..fd448775 100644 --- a/packages/code-analyzer-sfge-engine/src/engine.ts +++ b/packages/code-analyzer-sfge-engine/src/engine.ts @@ -31,10 +31,9 @@ export class SfgeEngine extends Engine { public constructor(config: SfgeEngineConfig, clock: Clock = new RealClock()) { super(); - // TODO: When we support custom Java commands, we'll need to use the config property instead of the hardcoded string here. - const javaCommandExecutor: JavaCommandExecutor = new JavaCommandExecutor('java', this.emitLogEvent.bind(this)); - this.sfgeWrapper = new RuntimeSfgeWrapper(javaCommandExecutor, clock, this.emitLogEvent.bind(this)); this.config = config; + const javaCommandExecutor: JavaCommandExecutor = new JavaCommandExecutor(this.config.java_command, this.emitLogEvent.bind(this)); + this.sfgeWrapper = new RuntimeSfgeWrapper(javaCommandExecutor, clock, this.emitLogEvent.bind(this)); } public override getName(): string { @@ -96,7 +95,7 @@ export class SfgeEngine extends Engine { const sfgeResults: SfgeRunResult[] = await this.sfgeWrapper.invokeRunCommand( selectedRuleInfoList, - relevantFiles, // TODO: WHEN WE ADD PATH-START TARGETING, THIS NEEDS TO CHANGE. + runOptions.pathStartPoints ?? [], relevantFiles, sfgeRunOptions, (innerPerc: number, message?: string) => this.emitRunRulesProgressEvent(5 + 93*innerPerc/100, message) // 5%-98% diff --git a/packages/code-analyzer-sfge-engine/src/sfge-wrapper.ts b/packages/code-analyzer-sfge-engine/src/sfge-wrapper.ts index 3fd7e898..295f9da3 100644 --- a/packages/code-analyzer-sfge-engine/src/sfge-wrapper.ts +++ b/packages/code-analyzer-sfge-engine/src/sfge-wrapper.ts @@ -3,6 +3,7 @@ import fs from 'node:fs'; import { getMessageFromCatalog, LogLevel, + PathPoint, SHARED_MESSAGE_CATALOG } from '@salesforce/code-analyzer-engine-api'; import {createTempDir, JavaCommandExecutor} from '@salesforce/code-analyzer-engine-api/utils'; @@ -108,7 +109,7 @@ export class RuntimeSfgeWrapper { } } - public async invokeRunCommand(selectedRuleInfos: SfgeRuleInfo[], targetPaths: string[], projectFilePaths: string[], sfgeRunOptions: SfgeRunOptions, emitProgress: (percComplete: number) => void): Promise { + public async invokeRunCommand(selectedRuleInfos: SfgeRuleInfo[], pathStarts: PathPoint[], projectFilePaths: string[], sfgeRunOptions: SfgeRunOptions, emitProgress: (percComplete: number) => void): Promise { const tmpDir: string = await this.getTemporaryWorkingDir(); emitProgress(2); @@ -116,7 +117,7 @@ export class RuntimeSfgeWrapper { const logFilePath: string = path.join(sfgeRunOptions.logFolder, this.logFileName); const ruleNames: string[] = selectedRuleInfos.map(sri => sri.name); - await this.createSfgeInputFile(inputFileName, ruleNames, targetPaths, projectFilePaths); + await this.createSfgeInputFile(inputFileName, ruleNames, pathStarts, projectFilePaths); const resultsOutputFile: string = path.join(tmpDir, 'resultsFile.json'); this.emitLogEvent(LogLevel.Debug, getMessage('LoggingToFile', 'run', logFilePath)); emitProgress(10); @@ -163,15 +164,29 @@ export class RuntimeSfgeWrapper { return this.temporaryWorkingDir; } - private async createSfgeInputFile(filePath: string, rules: string[], targets: string[], allWorkspaceFiles: string[]): Promise { - const sfgeTargets: SfgeTarget[] = targets.map(target => { - return { - targetFile: target, - targetMethods: [] - }; - }); + private async createSfgeInputFile(filePath: string, rules: string[], pathStarts: PathPoint[], allWorkspaceFiles: string[]): Promise { + const sfgeTargetsByFile: Map = new Map(); + if (pathStarts.length > 0) { + pathStarts.forEach(pathStart => { + const sfgeTarget: SfgeTarget = sfgeTargetsByFile.get(pathStart.file) ?? { + targetFile: pathStart.file, + targetMethods: [] + }; + if (pathStart.methodName) { + sfgeTarget.targetMethods = [...sfgeTarget.targetMethods, pathStart.methodName]; + } + sfgeTargetsByFile.set(pathStart.file, sfgeTarget); + }); + } else { + allWorkspaceFiles.map(file => { + sfgeTargetsByFile.set(file, { + targetFile: file, + targetMethods: [] + }); + }); + } const inputFileContents: SfgeInputFile = { - targets: sfgeTargets, + targets: [...sfgeTargetsByFile.values()], projectDirs: allWorkspaceFiles, rulesToRun: rules }; diff --git a/packages/code-analyzer-sfge-engine/test/engine.test.ts b/packages/code-analyzer-sfge-engine/test/engine.test.ts index d0503697..f0134626 100644 --- a/packages/code-analyzer-sfge-engine/test/engine.test.ts +++ b/packages/code-analyzer-sfge-engine/test/engine.test.ts @@ -8,6 +8,7 @@ import { EventType, LogEvent, LogLevel, + PathPoint, RuleDescription, RunOptions, RunRulesProgressEvent, @@ -182,10 +183,10 @@ describe('SfgeEngine', () => { path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace', 'SomeOtherClass.cls') ] } - ])('When workspace is $case, those violations are returned', async () => { + ])('When workspace is $case, those violations are returned', async ({workspacePaths}) => { // ====== SETUP ====== const engine: SfgeEngine = new SfgeEngine(DEFAULT_SFGE_ENGINE_CONFIG, fixedClock); - const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace')]); + const workspace: Workspace = new Workspace(workspacePaths); const progressEvents: RunRulesProgressEvent[] = []; engine.onEvent(EventType.RunRulesProgressEvent, (e: RunRulesProgressEvent) => progressEvents.push(e)); const ruleNames: string[] = ['ApexFlsViolationRule', 'UseWithSharingOnDatabaseOperation', 'UnimplementedTypeRule', 'RemoveUnusedMethod']; @@ -194,7 +195,46 @@ describe('SfgeEngine', () => { const results: EngineRunResults = await engine.runRules(ruleNames, createRunOptions(workspace)); // ====== ASSERTIONS ====== - await expectResultsToMatchGoldfile(results, 'all_sampleRelevantWorkspace_violations.goldfile.json', path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace')); + await expectResultsToMatchGoldfile(results, path.join('sampleRelevantWorkspace', 'all_violations.goldfile.json'), path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace')); + const progressPercents: number[] = progressEvents.map(pe => pe.percentComplete); + expect(progressPercents[0]).toEqual(2); + expect(progressPercents[progressPercents.length - 1]).toEqual(100); + expectProgressEventsToAscend(progressPercents); + }); + + it.each([ + { + case: 'a file that violates the selected rules', + pathStartPoints: [ + { + file: path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace2', 'EntryClass1.cls') + } + ], + goldfile: 'EntryClass1_violations.goldfile.json' + }, + { + case: 'a single method that violates the selected rules', + pathStartPoints: [ + { + file: path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace2', 'EntryClass1.cls'), + methodName: 'entryPointMethod1' + } + ], + goldfile: 'EntryClass1_entryPointMethod1_violations.goldfile.json' + } + ])('When path start point is $case, the appropriate violations are returned', async ({pathStartPoints, goldfile}) => { + // ====== SETUP ====== + const engine: SfgeEngine = new SfgeEngine(DEFAULT_SFGE_ENGINE_CONFIG, fixedClock); + const workspace: Workspace = new Workspace([path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace2')]); + const progressEvents: RunRulesProgressEvent[] = []; + engine.onEvent(EventType.RunRulesProgressEvent, (e: RunRulesProgressEvent) => progressEvents.push(e)); + const ruleNames: string[] = ['ApexFlsViolationRule', 'UseWithSharingOnDatabaseOperation', 'UnimplementedTypeRule', 'RemoveUnusedMethod']; + + // ====== TESTED BEHAVIOR ====== + const results: EngineRunResults = await engine.runRules(ruleNames, createRunOptions(workspace, pathStartPoints)); + + // ====== ASSERTIONS ====== + await expectResultsToMatchGoldfile(results, path.join('sampleRelevantWorkspace2', goldfile), path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace2')); const progressPercents: number[] = progressEvents.map(pe => pe.percentComplete); expect(progressPercents[0]).toEqual(2); expect(progressPercents[progressPercents.length - 1]).toEqual(100); @@ -213,7 +253,7 @@ describe('SfgeEngine', () => { const results: EngineRunResults = await engine.runRules(ruleNames, createRunOptions(workspace)); // ====== ASSERTIONS ====== - await expectResultsToMatchGoldfile(results, 'ApexFlsViolationRule_sampleRelevantWorkspace_violations.goldfile.json', path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace')); + await expectResultsToMatchGoldfile(results, path.join('sampleRelevantWorkspace', 'ApexFlsViolationRule_violations.goldfile.json'), path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace')); const expectedProgressDescriptors: {percent: number, message?: string}[] = [ {percent: 2, message: undefined}, {percent: 2.3, message: undefined}, @@ -292,7 +332,7 @@ describe('SfgeEngine', () => { const results: EngineRunResults = await engine.runRules(ruleNames, createRunOptions(workspace)); // ====== ASSERTIONS ====== - await expectResultsToMatchGoldfile(results, 'ApexFlsViolationRule_sampleRelevantWorkspace_violations.goldfile.json', path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace')); + await expectResultsToMatchGoldfile(results, path.join('sampleRelevantWorkspace', 'ApexFlsViolationRule_violations.goldfile.json'), path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace')); const warningLogEvents: LogEvent[] = logEvents.filter(e => e.logLevel === LogLevel.Warn); expect(warningLogEvents.length).toBeGreaterThanOrEqual(1); expect(warningLogEvents[0].message).toContain(`Specified workspace is missing 1 possibly-relevant file(s) from the folder ${path.join(TEST_DATA_FOLDER, 'sampleRelevantWorkspace')}.`); @@ -333,9 +373,10 @@ function createDescribeOptions(workspace?: Workspace): DescribeOptions { }; } -function createRunOptions(workspace: Workspace): RunOptions { +function createRunOptions(workspace: Workspace, pathStartPoints?: PathPoint[]): RunOptions { return { logFolder: os.tmpdir(), - workspace + workspace, + pathStartPoints }; } diff --git a/packages/code-analyzer-sfge-engine/test/test-data/goldfiles/ApexFlsViolationRule_sampleRelevantWorkspace_violations.goldfile.json b/packages/code-analyzer-sfge-engine/test/test-data/goldfiles/sampleRelevantWorkspace/ApexFlsViolationRule_violations.goldfile.json similarity index 100% rename from packages/code-analyzer-sfge-engine/test/test-data/goldfiles/ApexFlsViolationRule_sampleRelevantWorkspace_violations.goldfile.json rename to packages/code-analyzer-sfge-engine/test/test-data/goldfiles/sampleRelevantWorkspace/ApexFlsViolationRule_violations.goldfile.json diff --git a/packages/code-analyzer-sfge-engine/test/test-data/goldfiles/all_sampleRelevantWorkspace_violations.goldfile.json b/packages/code-analyzer-sfge-engine/test/test-data/goldfiles/sampleRelevantWorkspace/all_violations.goldfile.json similarity index 100% rename from packages/code-analyzer-sfge-engine/test/test-data/goldfiles/all_sampleRelevantWorkspace_violations.goldfile.json rename to packages/code-analyzer-sfge-engine/test/test-data/goldfiles/sampleRelevantWorkspace/all_violations.goldfile.json diff --git a/packages/code-analyzer-sfge-engine/test/test-data/goldfiles/sampleRelevantWorkspace2/EntryClass1_entryPointMethod1_violations.goldfile.json b/packages/code-analyzer-sfge-engine/test/test-data/goldfiles/sampleRelevantWorkspace2/EntryClass1_entryPointMethod1_violations.goldfile.json new file mode 100644 index 00000000..901e3468 --- /dev/null +++ b/packages/code-analyzer-sfge-engine/test/test-data/goldfiles/sampleRelevantWorkspace2/EntryClass1_entryPointMethod1_violations.goldfile.json @@ -0,0 +1,21 @@ +{ + "violations": [ + { + "ruleName": "ApexFlsViolationRule", + "message": "FLS validation is missing for [READ] operation on [Account] with field(s) [CustomField__c,Name].", + "codeLocations": [ + { + "file": "{{RUNDIR}}EntryClass1.cls", + "startLine": 2, + "startColumn": 24 + }, + { + "file": "{{RUNDIR}}InternalOnlyClass.cls", + "startLine": 5, + "startColumn": 26 + } + ], + "primaryLocationIndex": 1 + } + ] +} \ No newline at end of file diff --git a/packages/code-analyzer-sfge-engine/test/test-data/goldfiles/sampleRelevantWorkspace2/EntryClass1_violations.goldfile.json b/packages/code-analyzer-sfge-engine/test/test-data/goldfiles/sampleRelevantWorkspace2/EntryClass1_violations.goldfile.json new file mode 100644 index 00000000..472d85ef --- /dev/null +++ b/packages/code-analyzer-sfge-engine/test/test-data/goldfiles/sampleRelevantWorkspace2/EntryClass1_violations.goldfile.json @@ -0,0 +1,38 @@ +{ + "violations": [ + { + "ruleName": "ApexFlsViolationRule", + "message": "FLS validation is missing for [READ] operation on [Account] with field(s) [CustomField__c,Name].", + "codeLocations": [ + { + "file": "{{RUNDIR}}EntryClass1.cls", + "startLine": 2, + "startColumn": 24 + }, + { + "file": "{{RUNDIR}}InternalOnlyClass.cls", + "startLine": 5, + "startColumn": 26 + } + ], + "primaryLocationIndex": 1 + }, + { + "ruleName": "ApexFlsViolationRule", + "message": "FLS validation is missing for [READ] operation on [Account] with field(s) [CustomField__c,Name].", + "codeLocations": [ + { + "file": "{{RUNDIR}}EntryClass1.cls", + "startLine": 7, + "startColumn": 24 + }, + { + "file": "{{RUNDIR}}InternalOnlyClass.cls", + "startLine": 5, + "startColumn": 26 + } + ], + "primaryLocationIndex": 1 + } + ] +} \ No newline at end of file diff --git a/packages/code-analyzer-sfge-engine/test/test-data/sampleRelevantWorkspace2/EntryClass1.cls b/packages/code-analyzer-sfge-engine/test/test-data/sampleRelevantWorkspace2/EntryClass1.cls new file mode 100644 index 00000000..90d5ab7a --- /dev/null +++ b/packages/code-analyzer-sfge-engine/test/test-data/sampleRelevantWorkspace2/EntryClass1.cls @@ -0,0 +1,11 @@ +global with sharing class EntryClass1 { + global static void entryPointMethod1() { + InternalOnlyClass i = new InternalOnlyClass(); + i.doAnInsecureQuery(); + } + + global static void entryPointMethod2() { + InternalOnlyClass i = new InternalOnlyClass(); + i.doAnInsecureQuery(); + } +} diff --git a/packages/code-analyzer-sfge-engine/test/test-data/sampleRelevantWorkspace2/EntryClass2.cls b/packages/code-analyzer-sfge-engine/test/test-data/sampleRelevantWorkspace2/EntryClass2.cls new file mode 100644 index 00000000..0cbe69c6 --- /dev/null +++ b/packages/code-analyzer-sfge-engine/test/test-data/sampleRelevantWorkspace2/EntryClass2.cls @@ -0,0 +1,11 @@ +global with sharing class EntryClass2 { + global static void entryPointMethod1() { + InternalOnlyClass i = new InternalOnlyClass(); + i.doAnInsecureQuery(); + } + + global static void entryPointMethod2() { + InternalOnlyClass i = new InternalOnlyClass(); + i.doAnInsecureQuery(); + } +} diff --git a/packages/code-analyzer-sfge-engine/test/test-data/sampleRelevantWorkspace2/InternalOnlyClass.cls b/packages/code-analyzer-sfge-engine/test/test-data/sampleRelevantWorkspace2/InternalOnlyClass.cls new file mode 100644 index 00000000..86f373d9 --- /dev/null +++ b/packages/code-analyzer-sfge-engine/test/test-data/sampleRelevantWorkspace2/InternalOnlyClass.cls @@ -0,0 +1,7 @@ +public inherited sharing class InternalOnlyClass { + + public void doAnInsecureQuery() { + // This query should be using the USER_MODE syntax for CRUD/FLS reasons. It doesn't, and is therefore insecure. + Account[] accs = [SELECT Name, CustomField__c FROM Account]; + } +}