diff --git a/messages/DefaultRuleManager.js b/messages/DefaultRuleManager.js
index 1f4fc5ede..aed6fcb9f 100644
--- a/messages/DefaultRuleManager.js
+++ b/messages/DefaultRuleManager.js
@@ -3,5 +3,8 @@ module.exports = {
"targetSkipped": "The specified target wasn't processed by any engines. Use the --engine parameter to select a different engine or specify a different target. Specified target: %s.",
"targetsSkipped": "The specified targets weren't processed by any engines: %s. Review your target and engine combinations and try again.",
"pathsDoubleProcessed": "One or more files were processed by eslint and eslint-lwc simultaneously. To remove possible duplicate violations, customize the targetPatterns property for eslint and eslint-lwc engines in %s on these files: %s.",
+ },
+ "error": {
+ "cannotRunDfaAndNonDfaConcurrently": "DFA engines %s cannot be run concurrently with non-DFA engines %s"
}
}
diff --git a/messages/RunOutputProcessor.js b/messages/RunOutputProcessor.js
new file mode 100644
index 000000000..7340a2432
--- /dev/null
+++ b/messages/RunOutputProcessor.js
@@ -0,0 +1,9 @@
+module.exports = {
+ "output": {
+ "engineSummaryTemplate": "Executed %s, found %s violation(s) across %s file(s).",
+ "noViolationsDetected": "Executed engines: %s. No rule violations found.",
+ "sevThresholdSummary": "Rule violations of severity %s or more severe were detected.",
+ "writtenToConsole": "Rule violations were logged to the console.",
+ "writtenToOutFile": "Rule violations were written to %s."
+ }
+}
diff --git a/messages/SfgeEngine.js b/messages/SfgeEngine.js
index 719947304..1b3cd6ba3 100644
--- a/messages/SfgeEngine.js
+++ b/messages/SfgeEngine.js
@@ -1,4 +1,9 @@
module.exports = {
- "spinnerStart": "Analyzing with Salesforce Graph Engine. See %s for details.",
- "pleaseWait": "Please wait"
+ "messages": {
+ "pleaseWait": "Please wait",
+ "spinnerStart": "Analyzing with Salesforce Graph Engine. See %s for details."
+ },
+ "errors": {
+ "failedWithoutProjectDir": `The --projectdir|-p flag is missing. Rerun your command with --projectdir|-p to allow Graph Engine to run, or with --engine|-e to exclude Graph Engine from execution.`
+ }
};
diff --git a/messages/run-common.js b/messages/run-common.js
new file mode 100644
index 000000000..5ee655cc6
--- /dev/null
+++ b/messages/run-common.js
@@ -0,0 +1,23 @@
+module.exports = {
+ "flags": {
+ "formatDescription": "specify results output format",
+ "formatDescriptionLong": "Specifies results output format written directly to the console.",
+ "normalizesevDescription": "return normalized severity 1 (high), 2 (moderate), and 3 (low), and the engine-specific severity",
+ "normalizesevDescriptionLong": "Returns normalized severity 1 (high), 2 (moderate), and 3 (low), and the engine-specific severity. For the html option, the normalized severity is displayed instead of the engine severity.",
+ "outfileDescription": "write output to a file",
+ "outfileDescriptionLong": "Writes output to a file.",
+ "projectdirDescription": "provide root directory of project",
+ "projectdirDescriptionLong": "Provides the relative or absolute root project directory used to set the context for Graph Engine's analysis. Project directory must be a path, not a glob. Specify multiple values as a comma-separated list.",
+ "sevthresholdDescription": "throw an error when a violation threshold is reached, the --normalize-severity is invoked, and severity levels are reset to the baseline",
+ "sevthresholdDescriptionLong": "Throws an error when violations are found with equal or greater severity than the provided value. Values are 1 (high), 2 (moderate), and 3 (low). Exit code is the most severe violation. Using this flag also invokes the --normalize-severity flag."
+ },
+ "validations": {
+ "cannotWriteTableToFile": "Format 'table' can't be written to a file. Specify a different format.",
+ "outfileFormatMismatch": "The selected output format doesn't match the output file type. Output format: %s. Output file type: %s.",
+ "outfileMustBeValid": "--outfile must be a well-formed filepath.",
+ "outfileMustBeSupportedType": "--outfile must be of a supported type: .csv; .xml; .json; .html; .sarif.",
+ "projectdirCannotBeGlob": "--projectdir cannot specify globs",
+ "projectdirMustBeDir": "--projectdir must specify directories",
+ "projectdirMustExist": "--projectdir must specify existing paths"
+ }
+}
diff --git a/messages/run-dfa.js b/messages/run-dfa.js
index 55740907c..414e0d08e 100644
--- a/messages/run-dfa.js
+++ b/messages/run-dfa.js
@@ -3,22 +3,12 @@ module.exports = {
"commandDescriptionLong": `Scans codebase with all DFA rules by default.
Specify the format of output and print results directly or as contents of a file that you provide with --outfile flag.`,
"flags": {
- "formatDescription": "specify results output format",
- "formatDescriptionLong": "Specifies results output format written directly to the console.",
- "normalizesevDescription": "return normalized severity in addition to the engine-specific severity",
- "normalizesevDescriptionLong": "Returns normalized severity 1 (high), 2 (moderate), and 3 (low) and the engine-specific severity. For the html option, normalized severity is displayed instead of the engine severity.",
- "outfileDescription": "write output to a file",
- "outfileDescriptionLong": "Writes output to a file.",
- "projectdirDescription": "provide root directory of project",
- "projectdirDescriptionLong": "Provides the relative or absolute root project directory used to set the context for Graph Engine's analysis. Project directory must be a path, not a glob. Specify multiple values as a comma-separated list.",
"ruledisablewarningviolationDescription": "disable warning violations from Salesforce Graph Engine. Alternatively, set value using environment variable `SFGE_RULE_DISABLE_WARNING_VIOLATION`",
"ruledisablewarningviolationDescriptionLong": "Disables warning violations, such as those on StripInaccessible READ access, to get only high-severity violations (default: false). Inherits value from SFGE_RULE_DISABLE_WARNING_VIOLATION env-var if set.",
"rulethreadcountDescription": "specify number of threads that evaluate DFA rules. Alternatively, set value using environment variable `SFGE_RULE_THREAD_COUNT`. Default is 4",
"rulethreadcountDescriptionLong": "Specifies number of rule evaluation threads, or how many entrypoints can be evaluated concurrently. Inherits value from SFGE_RULE_THREAD_COUNT env-var, if set. Default is 4.",
"rulethreadtimeoutDescription": "specify timeout for individual rule threads in milliseconds. Alternatively, set the timeout value using environment variable `SFGE_RULE_THREAD_TIMEOUT`. Default: 90000 ms",
"rulethreadtimeoutDescriptionLong": "Specifies time limit for evaluating a single entrypoint in milliseconds. Inherits value from SFGE_RULE_THREAD_TIMEOUT env-var if set. Default is 900,000 ms, or 15 minutes.",
- "sevthresholdDescription": "throw an error when violations of specific or higher severity are detected, and invoke --normalize-severity",
- "sevthresholdDescriptionLong": "Throws an error when violations are found with equal or greater severity than provided value. Values are 1 (high), 2 (moderate), and 3 (low). Exit code is the most severe violation. Using this flag also invokes the --normalize-severity flag.",
"sfgejvmargsDescription": "specify Java Virtual Machine (JVM) arguments to optimize Salesforce Graph Engine execution to your system (optional)",
"sfgejvmargsDescriptionLong": "Specifies Java Virtual Machine arguments to override system defaults while executing Salesforce Graph Engine. For multiple arguments, add them to the same string separated by space.",
"targetDescription": "return location of source code",
@@ -27,12 +17,10 @@ module.exports = {
"validations": {
"methodLevelTargetCannotBeGlob": "Method-level targets supplied to --target cannot be globs",
"methodLevelTargetMustBeRealFile": "Method-level target %s must be a real file",
- "projectdirCannotBeGlob": "--projectdir cannot specify globs",
- "projectdirMustBeDir": "--projectdir must specify directories",
- "projectdirMustExist": "--projectdir must specify existing paths"
+ "projectdirIsRequired": "--projectdir is required for this command.",
},
"examples": `The paths specified for --projectdir must contain all files specified through --target cumulatively.
- $ sfdx sacnner:run:dfa --target "./myproject/main/default/classes/*.cls" --projectdir "./myproject/"
+ $ sfdx scanner:run:dfa --target "./myproject/main/default/classes/*.cls" --projectdir "./myproject/"
$ sfdx scanner:run:dfa --target "./**/*.cls" --projectdir "./"
$ sfdx scanner:run:dfa --target "./dir1/file1.cls,./dir2/file2.cls" --projectdir "./dir1/,./dir2/"
This example fails because the set of files included in --target is larger than that contained in --projectdir:
diff --git a/messages/run.js b/messages/run-pathless.js
similarity index 74%
rename from messages/run.js
rename to messages/run-pathless.js
index 5812aeaae..cba4b3264 100644
--- a/messages/run.js
+++ b/messages/run-pathless.js
@@ -8,19 +8,11 @@ module.exports = {
"rulesetDescriptionLong": "[deprecated] One or more rulesets to run. Specify multiple values as a comma-separated list.",
"targetDescription": "source code location",
"targetDescriptionLong": "Source code location. May use glob patterns. Specify multiple values as a comma-separated list.",
- "formatDescription": "specify results output format",
- "formatDescriptionLong": "Specifies output format with results written directly to the console.",
- "outfileDescription": "write output to a file",
- "outfileDescriptionLong": "Writes output to a file.",
"envDescription": "[deprecated] override ESLint's default environment variables, in JSON-formatted string",
"envDescriptionLong": "[deprecated] Overrides ESLint's default environmental variables, in JSON-formatted string.",
"envParamDeprecationWarning": "--env parameter is being deprecated, and will be removed in a future release.",
"tsconfigDescription": "location of tsconfig.json file",
"tsconfigDescriptionLong": "Location of tsconfig.json file used by eslint-typescript engine.",
- "stDescription": "throw an error when a violation threshold is reached, the --normalize-severity is invoked, and severity levels are reset to the baseline",
- "stDescriptionLong": "Throws an error when violations are found with equal or greater severity than the provided value. --normalize-severity is invoked and severity levels are reset to the baseline. Normalized severity values are: 1 (high), 2 (moderate), and 3 (low). Exit code is the most severe violation.",
- "nsDescription": "return normalized severity 1 (high), 2 (moderate), and 3 (low), and the engine-specific severity",
- "nsDescriptionLong": "Returns normalized severity 1 (high), 2 (moderate), and 3 (low), and the engine-specific severity. For the html option, the normalized severity is displayed instead of the engine severity.",
'engineDescription': "specify which engines to run",
'engineDescriptionLong': "Specifies one or more engines to run. Submit multiple values as a comma-separated list.",
'eslintConfigDescription': 'specify the location of eslintrc config to customize eslint engine',
@@ -28,24 +20,14 @@ module.exports = {
'pmdConfigDescription': 'specify location of PMD rule reference XML file to customize rule selection',
'pmdConfigDescriptionLong': 'Specifies the location of PMD rule reference XML file to customize rule selection.',
"verboseViolationsDescription": "return retire-js violation message details",
- "verboseViolationsDescriptionLong": "Returns retire-js violation messages details about each vulnerability, including summary, Common Vulnerabilities and Exposures (CVE), and URLs."
+ "verboseViolationsDescriptionLong": "Returns retire-js violation messages details about each vulnerability, including summary, Common Vulnerabilities and Exposures (CVE), and URLs."
},
"validations": {
"methodLevelTargetingDisallowed": "The target '%s' is invalid because method-level targeting isn't supported with this command.",
- "outfileFormatMismatch": "The selected output format doesn't match the output file type. Output format: %s. Output file type: %s.",
- "outfileMustBeValid": "--outfile must be a well-formed filepath.",
- "outfileMustBeSupportedType": "--outfile must be of a supported type: .csv; .xml; .json; .html; .sarif.",
- "cannotWriteTableToFile": "Format 'table' can't be written to a file. Specify a different format.",
"tsConfigEslintConfigExclusive": "A --tsconfig flag can't be specified with an --eslintconfig flag. Review your tsconfig path in the eslint config file under 'parseOptions.project'.",
},
"output": {
- "noViolationsDetected": "Executed engines: %s. No rule violations found.",
"invalidEnvJson": "--env parameter must be a well-formed JSON.",
- "engineSummaryTemplate": "Executed %s, found %s violation(s) across %s file(s).",
- "writtenToOutFile": "Rule violations were written to %s.",
- "writtenToConsole": "Rule violations were logged to the console.",
- "sevThresholdSummary": "Rule violations of severity %s or more severe were detected.",
- "pleaseSeeAbove": "Review the logs.",
"filtersIgnoredCustom": "Rule filters will be ignored by engines that are run with custom config using --pmdconfig or --eslintconfig flags. Modify your config file to include your filters."
},
"rulesetDeprecation": "The 'ruleset' command parameter is deprecated. Use 'category' instead.",
@@ -96,5 +78,13 @@ This example uses --normalize-severity to output normalized severity and engine-
This example uses --severity-threshold to throw a non-zero exit code when rule violations of normalized severity 2 or greater are found. If any violations with the specified severity (or greater) are found, the exit code equals the severity of the most severe violation.
$ sfdx scanner:run --target "/some-project/" --severity-threshold 2
+
+The paths specified for --projectdir must contain all files specified through --target cumulatively.
+ $ sfdx scanner:run --target "./myproject/main/default/classes/*.cls" --projectdir "./myproject/"
+ $ sfdx scanner:run --target "./**/*.cls" --projectdir "./"
+ $ sfdx scanner:run --target "./dir1/file1.cls,./dir2/file2.cls" --projectdir "./dir1/,./dir2/"
+
+This example fails because the set of files included in --target is larger than that contained in --projectdir:
+ $ sfdx scanner:run --target "./**/*.cls" --projectdir "./myproject/"
`
};
diff --git a/package.json b/package.json
index 4aa2a4ddb..3b428b019 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@salesforce/sfdx-scanner",
"description": "Static code scanner that applies quality and security rules to Apex code, and provides feedback.",
- "version": "3.7.1",
+ "version": "3.8.0",
"author": "ISV SWAT",
"bugs": "https://github.com/forcedotcom/sfdx-scanner/issues",
"dependencies": {
@@ -22,7 +22,7 @@
"eslint": "^8.10.0",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.1",
- "find-java-home": "1.1.0",
+ "find-java-home": "1.2.2",
"globby": "^11.0.0",
"html-escaper": "^3.0.0",
"is-zip": "^1.0.0",
diff --git a/sfge/src/main/java/com/salesforce/Main.java b/sfge/src/main/java/com/salesforce/Main.java
index 9b7e7ed6a..9ffb38b63 100644
--- a/sfge/src/main/java/com/salesforce/Main.java
+++ b/sfge/src/main/java/com/salesforce/Main.java
@@ -17,7 +17,6 @@
import com.salesforce.rules.AbstractRule;
import com.salesforce.rules.AbstractRuleRunner;
import com.salesforce.rules.RuleRunner;
-import com.salesforce.rules.RuleUtil;
import com.salesforce.rules.Violation;
import com.salesforce.rules.ops.ProgressListenerProvider;
import java.util.Arrays;
@@ -28,30 +27,28 @@
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
/**
- * The main class, invoked by sfdx-scanner. `catalog` flow lists all of the available rules in a
- * standardized format.
- *
- *
The `execute` flow accepts as a parameter the name of a file whose contents are a JSON with
- * the following structure:
+ * The main class, invoked by sfdx-scanner. The first arg should be either `catalog` or `execute`,
+ * and determines which flow runs.
+ * For `catalog`, the second arg should be either `pathless` or `dfa`. Enabled rules matching that
+ * type will be logged as a JSON. No meaningful environment variables exist for this flow. Exit code
+ * 0 means success. Any other exit code means failure.
+ * For `execute`, the second arg should be the path to a JSON file whose contents are structured as:
*
*
- *
rulesToRun: An array of rule names.
- *
projectDirs: An array of directories from which the graph should be built.
- *
targets: An array of objects with a `targetFile` property indicating the file to be
- * analyzed and a `targetMethods` property indicating individual methods.
+ *
rulesToRun: an array of rule names.
+ *
projectDirs: an array of directories from which the graph should be built.
+ *
targets: an array of objects with a `targetFile` property indicating the file to be
+ * analyzed and a `targetMethods` property that may optionally indicate individual methods
+ * within that file.
*
*
- *
Exit codes:
- *
- *
- *
Negative numbers indicate an internal error.
- *
0 indicates a successful run with * no violations.
- *
Positive numbers indicate a successful run with exit-code-many violations.
- *
+ * The following exit codes are possible:
*
- *
Usage: mvn exec:java -Dexec.mainClass=com.salesforce.Main -Dexec.args="catalog" OR mvn
- * exec:java -Dexec.mainClass=com.salesforce.Main -Dexec.args="execute [path to file listing
- * targets] [path to file listing sources] [comma-separated rules]"
+ *
+ *
0: Successful run without violations.
+ *
1: Internal error with no violations.
+ *
4: Successful run with violations.5: Internal error with some violations.>
+ *
*/
@SuppressWarnings(
"PMD.SystemPrintln") // Since println is currently used to communicate to outer layer
@@ -97,7 +94,7 @@ int process(String... args) {
switch (action) {
case CATALOG:
- return catalog();
+ return catalog(args);
case EXECUTE:
return execute(args);
default:
@@ -105,11 +102,14 @@ int process(String... args) {
}
}
- private int catalog() {
+ /** Expectations for args documented in class header above. */
+ private int catalog(String... args) {
LOGGER.info("Invoked CATALOG flow");
+ CliArgParser.CatalogArgParser cap = new CliArgParser.CatalogArgParser();
List rules;
try {
- rules = RuleUtil.getEnabledRules();
+ cap.parseArgs(args);
+ rules = cap.getSelectedRules();
} catch (SfgeException | SfgeRuntimeException ex) {
dependencies.printError(ex.getMessage());
return EXIT_WITH_INTERNAL_ERROR_NO_VIOLATIONS;
@@ -119,6 +119,7 @@ private int catalog() {
return EXIT_GOOD_RUN_NO_VIOLATIONS;
}
+ /** Expectations for args documented in class header above. */
private int execute(String... args) {
LOGGER.info("Invoked EXECUTE flow");
// Parse the arguments with our delegate class.
diff --git a/sfge/src/main/java/com/salesforce/apex/jorje/ASTConstants.java b/sfge/src/main/java/com/salesforce/apex/jorje/ASTConstants.java
index 37a862002..7ca20dd14 100644
--- a/sfge/src/main/java/com/salesforce/apex/jorje/ASTConstants.java
+++ b/sfge/src/main/java/com/salesforce/apex/jorje/ASTConstants.java
@@ -27,6 +27,7 @@
import apex.jorje.semantic.ast.expression.ReferenceExpression;
import apex.jorje.semantic.ast.expression.SoqlExpression;
import apex.jorje.semantic.ast.expression.SoslExpression;
+import apex.jorje.semantic.ast.expression.SuperMethodCallExpression;
import apex.jorje.semantic.ast.expression.ThisMethodCallExpression;
import apex.jorje.semantic.ast.expression.ThisVariableExpression;
import apex.jorje.semantic.ast.expression.TriggerVariableExpression;
@@ -145,6 +146,8 @@ public static String getVertexLabel(Class extends AstNode> clazz) {
public static final String REFERENCE_EXPRESSION = getVertexLabel(ReferenceExpression.class);
public static final String RETURN_STATEMENT = getVertexLabel(ReturnStatement.class);
public static final String STANDARD_CONDITION = getVertexLabel(StandardCondition.class);
+ public static final String SUPER_METHOD_CALL_EXPRESSION =
+ getVertexLabel(SuperMethodCallExpression.class);
public static final String THIS_METHOD_CALL_EXPRESSION =
getVertexLabel(ThisMethodCallExpression.class);
public static final String THIS_VARIABLE_EXPRESSION =
diff --git a/sfge/src/main/java/com/salesforce/cli/CliArgParser.java b/sfge/src/main/java/com/salesforce/cli/CliArgParser.java
index 9b0558bca..679f8a822 100644
--- a/sfge/src/main/java/com/salesforce/cli/CliArgParser.java
+++ b/sfge/src/main/java/com/salesforce/cli/CliArgParser.java
@@ -41,6 +41,52 @@ public CLI_ACTION getCliAction(String... args) {
}
}
+ public static class CatalogArgParser {
+ private static final int ARG_COUNT = 2;
+ // NOTE: This value must match the one for the RuleType enum declared in Constants.ts.
+ private static final String PATHLESS = "pathless";
+ // NOTE: This value must match the one for the RuleType enum declared in Constants.ts.
+ private static final String DFA = "dfa";
+
+ private List selectedRules;
+
+ public CatalogArgParser() {
+ selectedRules = new ArrayList<>();
+ }
+
+ /**
+ * See the documentation of {@link com.salesforce.Main} for information about the
+ * expectations for args.
+ */
+ public void parseArgs(String... args) throws RuleUtil.RuleNotFoundException {
+ if (LOGGER.isInfoEnabled()) {
+ LOGGER.info("CLI args received: " + Arrays.toString(args));
+ }
+ // Make sure we have the right number of arguments.
+ if (args.length != ARG_COUNT) {
+ throw new InvocationException(
+ String.format(
+ "Wrong number of arguments. Expected %d; received %d",
+ ARG_COUNT, args.length));
+ }
+ switch (args[1]) {
+ case PATHLESS:
+ selectedRules = RuleUtil.getEnabledStaticRules();
+ break;
+ case DFA:
+ selectedRules = RuleUtil.getEnabledPathBasedRules();
+ break;
+ default:
+ selectedRules = RuleUtil.getEnabledRules();
+ break;
+ }
+ }
+
+ public List getSelectedRules() {
+ return selectedRules;
+ }
+ }
+
public static class ExecuteArgParser {
private static int ARG_COUNT = 2;
@@ -62,6 +108,10 @@ public ExecuteArgParser(Dependencies dependencies) {
this.dependencies = dependencies;
}
+ /**
+ * See the documentation of {@link com.salesforce.Main} for information about the
+ * expectations for args.
+ */
public void parseArgs(String... args) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("CLI args received: " + Arrays.toString(args));
@@ -92,28 +142,6 @@ public List getSelectedRules() {
return selectedRules;
}
- private void identifyProjectDirs(String inputFile) {
- try {
- projectDirs.addAll(readFile(inputFile));
- } catch (IOException ex) {
- throw new InvocationException(
- "Could not read source-list file " + inputFile + ": " + ex.getMessage(),
- ex);
- }
- }
-
- private void identifyTargetFiles(String inputFile) {
- try {
- String targetJson = String.join("\n", readFile(inputFile));
- Gson gson = new Gson();
- targets.addAll(Arrays.asList(gson.fromJson(targetJson, RuleRunnerTarget[].class)));
- } catch (IOException ex) {
- throw new InvocationException(
- "Could not read target-list file " + inputFile + ": " + ex.getMessage(),
- ex);
- }
- }
-
private ExecuteInput readInputFile(String fileName) {
try {
String inputJson = String.join("\n", readFile(fileName));
diff --git a/sfge/src/main/java/com/salesforce/config/UserFacingMessages.java b/sfge/src/main/java/com/salesforce/config/UserFacingMessages.java
index ad670ec93..c13b58b19 100644
--- a/sfge/src/main/java/com/salesforce/config/UserFacingMessages.java
+++ b/sfge/src/main/java/com/salesforce/config/UserFacingMessages.java
@@ -20,6 +20,9 @@ public final class UserFacingMessages {
public static final String UNREACHABLE_CODE =
"Remove unreachable code to proceed with the analysis: %s,%s:%d";
+ public static final String VARIABLE_DECLARED_MULTIPLE_TIMES =
+ "Rename or remove reused variable to proceed with analysis: %s,%s:%d";
+
/** CRUD/FLS Violation messages * */
// format: "CRUD" or "FLS", DML operation, Object type, Field information
public static final String VIOLATION_MESSAGE_TEMPLATE =
diff --git a/sfge/src/main/java/com/salesforce/exception/UserActionException.java b/sfge/src/main/java/com/salesforce/exception/UserActionException.java
index 621faea32..bc44d3115 100644
--- a/sfge/src/main/java/com/salesforce/exception/UserActionException.java
+++ b/sfge/src/main/java/com/salesforce/exception/UserActionException.java
@@ -8,4 +8,17 @@ public final class UserActionException extends SfgeRuntimeException {
public UserActionException(String message) {
super(message);
}
+
+ /**
+ * Construct structured user action exception
+ *
+ * @param messageTemplate typically from {@link com.salesforce.config.UserFacingMessages}
+ * @param filename where the issue was noticed
+ * @param definingType class name to fix
+ * @param lineNumber line number where the problem is
+ */
+ public UserActionException(
+ String messageTemplate, String filename, String definingType, int lineNumber) {
+ this(String.format(messageTemplate, filename, definingType, lineNumber));
+ }
}
diff --git a/sfge/src/main/java/com/salesforce/graph/Schema.java b/sfge/src/main/java/com/salesforce/graph/Schema.java
index f5b011cd1..1c839a6f9 100644
--- a/sfge/src/main/java/com/salesforce/graph/Schema.java
+++ b/sfge/src/main/java/com/salesforce/graph/Schema.java
@@ -23,11 +23,15 @@ public class Schema {
public static final String FIRST_CHILD = "FirstChild";
public static final String FULL_METHOD_NAME = "FullMethodName";
public static final String GLOBAL = "Global";
+ public static final String HANDLE_INBOUND_EMAIL = "handleInboundEmail";
public static final String HAS_GETTER_METHOD_BLOCK = "HasGetterMethodBlock";
public static final String HAS_SETTER_METHOD_BLOCK = "HasSetterMethodBlock";
public static final String IDENTIFIER = "Identifier";
public static final String IMPLEMENTATION_OF = "ImplementationOf";
public static final String IMPLEMENTED_BY = "ImplementedBy";
+ public static final String INBOUND_EMAIL_HANDLER = "Messaging.InboundEmailHandler";
+ public static final String INBOUND_EMAIL_RESULT = "Messaging.InboundEmailResult";
+ public static final String INSTANCE_CONSTRUCTOR_CANONICAL_NAME = "";
public static final String INTERFACE_DEFINING_TYPES = "InterfaceDefiningTypes";
public static final String INTERFACE_NAMES = "InterfaceNames";
public static final String INVOCABLE_METHOD = "InvocableMethod";
@@ -49,11 +53,13 @@ public class Schema {
public static final String NAMESPACE_ACCESSIBLE = "NamespaceAccessible";
public static final String OPERATOR = "Operator";
public static final String OVERRIDE = "Override";
+ public static final String PAGE_REFERENCE = "PageReference";
public static final String REFERENCE_TYPE = "ReferenceType";
public static final String REMOTE_ACTION = "RemoteAction";
public static final String RETURN_TYPE = "ReturnType";
public static final String RULE_NAMES = "RulesNames";
public static final String STATIC = "Static";
+ public static final String STATIC_CONSTRUCTOR_CANONICAL_NAME = "";
public static final String SUPER_CLASS_NAME = "SuperClassName";
public static final String SUPER_INTERFACE_NAME = "SuperInterfaceName";
public static final String TYPE = "Type";
diff --git a/sfge/src/main/java/com/salesforce/graph/build/StaticBlockUtil.java b/sfge/src/main/java/com/salesforce/graph/build/StaticBlockUtil.java
index e7ae15e86..290fbc769 100644
--- a/sfge/src/main/java/com/salesforce/graph/build/StaticBlockUtil.java
+++ b/sfge/src/main/java/com/salesforce/graph/build/StaticBlockUtil.java
@@ -5,7 +5,6 @@
import com.salesforce.collections.CollectionUtil;
import com.salesforce.exception.ProgrammingException;
import com.salesforce.graph.Schema;
-import com.salesforce.graph.ops.MethodUtil;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -342,7 +341,7 @@ private static void addProperties(
*/
private static boolean isClinitMethod(JorjeNode node) {
return ASTConstants.NodeType.METHOD.equals(node.getLabel())
- && MethodUtil.STATIC_CONSTRUCTOR_CANONICAL_NAME.equals(
+ && Schema.STATIC_CONSTRUCTOR_CANONICAL_NAME.equals(
node.getProperties().get(Schema.NAME));
}
diff --git a/sfge/src/main/java/com/salesforce/graph/ops/MethodUtil.java b/sfge/src/main/java/com/salesforce/graph/ops/MethodUtil.java
index 946b83d4e..b212e0318 100644
--- a/sfge/src/main/java/com/salesforce/graph/ops/MethodUtil.java
+++ b/sfge/src/main/java/com/salesforce/graph/ops/MethodUtil.java
@@ -2,7 +2,6 @@
import static com.salesforce.apex.jorje.ASTConstants.PROPERTY_METHOD_PREFIX;
import static com.salesforce.graph.ops.TypeableUtil.NOT_A_MATCH;
-import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.has;
import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.out;
import com.salesforce.apex.jorje.ASTConstants;
@@ -55,22 +54,18 @@
import com.salesforce.graph.visitor.DefaultNoOpPathVertexVisitor;
import com.salesforce.messaging.CliMessager;
import com.salesforce.messaging.EventKey;
-import com.salesforce.metainfo.MetaInfoCollectorProvider;
import com.salesforce.rules.AbstractRuleRunner.RuleRunnerTarget;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
-import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.apache.tinkerpop.gremlin.process.traversal.Order;
import org.apache.tinkerpop.gremlin.process.traversal.P;
-import org.apache.tinkerpop.gremlin.process.traversal.Scope;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
@@ -80,13 +75,6 @@
public final class MethodUtil {
private static final Logger LOGGER = LogManager.getLogger(MethodUtil.class);
- private static final String PAGE_REFERENCE = "PageReference";
- private static final String INBOUND_EMAIL_HANDLER = "Messaging.InboundEmailHandler";
- private static final String HANDLE_INBOUND_EMAIL = "handleInboundEmail";
- private static final String INBOUND_EMAIL_RESULT = "Messaging.InboundEmailResult";
- public static final String INSTANCE_CONSTRUCTOR_CANONICAL_NAME = "";
- public static final String STATIC_CONSTRUCTOR_CANONICAL_NAME = "";
-
public static List getTargetedMethods(
GraphTraversalSource g, List targets) {
// The targets passed into this method should exclusively be ones that target specific
@@ -163,175 +151,6 @@ private static void addMessagesForTarget(RuleRunnerTarget target, List getAuraEnabledMethods(
- GraphTraversalSource g, List targetFiles) {
- return getMethodsWithAnnotation(g, targetFiles, Schema.AURA_ENABLED);
- }
-
- /**
- * Returns non-test methods in the target files with a @NamespaceAccessible annotation. An empty
- * list implicitly includes all files.
- */
- public static List getNamespaceAccessibleMethods(
- GraphTraversalSource g, List targetFiles) {
- return getMethodsWithAnnotation(g, targetFiles, Schema.NAMESPACE_ACCESSIBLE);
- }
-
- /**
- * Returns non-test methods in the target files with a @RemoteAction annotation. An empty list
- * implicitly includes all files.
- */
- public static List getRemoteActionMethods(
- GraphTraversalSource g, List targetFiles) {
- return getMethodsWithAnnotation(g, targetFiles, Schema.REMOTE_ACTION);
- }
-
- /**
- * Returns non-test methods in the target files with an @InvocableMethod annotation. An empty
- * list implicitly includes all files.
- */
- public static List getInvocableMethodMethods(
- GraphTraversalSource g, List targetFiles) {
- return getMethodsWithAnnotation(g, targetFiles, Schema.INVOCABLE_METHOD);
- }
-
- static List getMethodsWithAnnotation(
- GraphTraversalSource g, List targetFiles, String annotation) {
- return SFVertexFactory.loadVertices(
- g,
- rootMethodTraversal(g, targetFiles)
- .where(
- out(Schema.CHILD)
- .hasLabel(NodeType.MODIFIER_NODE)
- .out(Schema.CHILD)
- .where(H.has(NodeType.ANNOTATION, Schema.NAME, annotation)))
- .order(Scope.global)
- .by(Schema.DEFINING_TYPE, Order.asc)
- .by(Schema.NAME, Order.asc));
- }
-
- /**
- * Returns non-test methods in the target files whose return type is a PageReference. An empty
- * list implicitly includes all files.
- */
- public static List getPageReferenceMethods(
- GraphTraversalSource g, List targetFiles) {
- return SFVertexFactory.loadVertices(
- g,
- rootMethodTraversal(g, targetFiles)
- .where(H.has(NodeType.METHOD, Schema.RETURN_TYPE, PAGE_REFERENCE))
- .order(Scope.global)
- .by(Schema.DEFINING_TYPE, Order.asc)
- .by(Schema.NAME, Order.asc));
- }
-
- private static GraphTraversal rootMethodTraversal(
- GraphTraversalSource g, List targetFiles) {
- // Only look at UserClass vertices. Not interested in Enums, Interfaces, or Triggers
- final String[] labels = new String[] {NodeType.USER_CLASS};
- return TraversalUtil.fileRootTraversal(g, labels, targetFiles)
- .not(has(Schema.IS_TEST, true))
- .repeat(__.out(Schema.CHILD))
- .until(__.hasLabel(NodeType.METHOD))
- .not(has(Schema.IS_TEST, true));
- }
-
- /**
- * Returns non-test methods in the target files whose modifier scope is `global`. An empty list
- * implicitly includes all files.
- */
- public static List getGlobalMethods(
- GraphTraversalSource g, List targetFiles) {
- // Get all methods in the target files.
- return SFVertexFactory.loadVertices(
- g,
- rootMethodTraversal(g, targetFiles)
- .filter(
- __.and(
- // If a method has at least one block statement, then it is
- // definitely actually declared, as
- // opposed to being an implicit method.
- out(Schema.CHILD)
- .hasLabel(NodeType.BLOCK_STATEMENT)
- .count()
- .is(P.gte(1)),
- // We only want global methods.
- out(Schema.CHILD)
- .hasLabel(NodeType.MODIFIER_NODE)
- .has(Schema.GLOBAL, true),
- // Ignore any standard methods, otherwise will get a ton of
- // extra results.
- __.not(__.has(Schema.IS_STANDARD, true)))));
- }
-
- public static List getInboundEmailHandlerMethods(
- GraphTraversalSource g, List targetFiles) {
- return SFVertexFactory.loadVertices(
- g,
- // Get any target class that implements the email handler interface.
- TraversalUtil.traverseImplementationsOf(g, targetFiles, INBOUND_EMAIL_HANDLER)
- // Get every implementation of the handle email method.
- .out(Schema.CHILD)
- .where(H.has(NodeType.METHOD, Schema.NAME, HANDLE_INBOUND_EMAIL))
- // Filter the results by return type and arity to limit the possibility of
- // getting unnecessary results.
- .where(H.has(NodeType.METHOD, Schema.RETURN_TYPE, INBOUND_EMAIL_RESULT))
- .has(Schema.ARITY, 2));
- }
-
- /**
- * Returns all non-test public- and global-scoped methods in controllers referenced by
- * VisualForce pages, filtered by target file list. An empty list implicitly includes all files.
- *
- * @param g
- * @param targetFiles
- * @return
- */
- public static List getExposedControllerMethods(
- GraphTraversalSource g, List targetFiles) {
- Set referencedVfControllers =
- MetaInfoCollectorProvider.getVisualForceHandler().getMetaInfoCollected();
- // If none of the VF files referenced an Apex class, we can just return an empty list.
- if (referencedVfControllers.isEmpty()) {
- return new ArrayList<>();
- }
- List allControllerMethods =
- SFVertexFactory.loadVertices(
- g,
- TraversalUtil.fileRootTraversal(g, targetFiles)
- // Only outer classes can be VF controllers, so we should restrict
- // our query to UserClasses.
- .where(
- H.hasWithin(
- NodeType.USER_CLASS,
- Schema.DEFINING_TYPE,
- referencedVfControllers))
- .repeat(__.out(Schema.CHILD))
- .until(__.hasLabel(NodeType.METHOD))
- .not(has(Schema.IS_TEST, true))
- // We want to ignore constructors.
- .where(
- __.not(
- H.hasWithin(
- NodeType.METHOD,
- Schema.NAME,
- INSTANCE_CONSTRUCTOR_CANONICAL_NAME,
- STATIC_CONSTRUCTOR_CANONICAL_NAME))));
- // Gremlin isn't sophisticated enough to perform this kind of filtering in the actual query.
- // So we'll just do it
- // manually here.
- return allControllerMethods.stream()
- .filter(
- methodVertex ->
- methodVertex.getModifierNode().isPublic()
- || methodVertex.getModifierNode().isGlobal())
- .collect(Collectors.toList());
- }
-
public static Optional getInvoked(
GraphTraversalSource g,
final String definingType,
@@ -439,7 +258,7 @@ public static Optional getInvoked(
H.has(
NodeType.METHOD,
Schema.NAME,
- INSTANCE_CONSTRUCTOR_CANONICAL_NAME))
+ Schema.INSTANCE_CONSTRUCTOR_CANONICAL_NAME))
.has(Schema.CONSTRUCTOR, true)
.has(Schema.ARITY, vertex.getParameters().size()));
@@ -464,7 +283,7 @@ public static Optional getNoArgConstructor(
H.has(
NodeType.METHOD,
Schema.NAME,
- INSTANCE_CONSTRUCTOR_CANONICAL_NAME))
+ Schema.INSTANCE_CONSTRUCTOR_CANONICAL_NAME))
.has(Schema.CONSTRUCTOR, true)
.has(Schema.ARITY, 0)));
}
@@ -482,7 +301,7 @@ public static List getNonDefaultConstructors(
H.has(
NodeType.METHOD,
Schema.NAME,
- INSTANCE_CONSTRUCTOR_CANONICAL_NAME))
+ Schema.INSTANCE_CONSTRUCTOR_CANONICAL_NAME))
.has(Schema.CONSTRUCTOR, true)
// User defined constructors have a block statement
.where(out(Schema.CHILD).hasLabel(NodeType.BLOCK_STATEMENT)));
@@ -513,7 +332,7 @@ public static Optional getInvoked(
H.has(
NodeType.METHOD,
Schema.NAME,
- INSTANCE_CONSTRUCTOR_CANONICAL_NAME))
+ Schema.INSTANCE_CONSTRUCTOR_CANONICAL_NAME))
.has(Schema.CONSTRUCTOR, true)
.has(Schema.ARITY, vertex.getParameters().size()));
if (methods.isEmpty()) {
@@ -541,7 +360,7 @@ public static Optional getInvoked(
H.has(
NodeType.METHOD,
Schema.NAME,
- INSTANCE_CONSTRUCTOR_CANONICAL_NAME))
+ Schema.INSTANCE_CONSTRUCTOR_CANONICAL_NAME))
.has(Schema.CONSTRUCTOR, true)
.has(Schema.ARITY, vertex.getParameters().size()));
diff --git a/sfge/src/main/java/com/salesforce/graph/ops/PathEntryPointUtil.java b/sfge/src/main/java/com/salesforce/graph/ops/PathEntryPointUtil.java
new file mode 100644
index 000000000..504a48525
--- /dev/null
+++ b/sfge/src/main/java/com/salesforce/graph/ops/PathEntryPointUtil.java
@@ -0,0 +1,323 @@
+package com.salesforce.graph.ops;
+
+import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.has;
+import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.out;
+
+import com.salesforce.apex.jorje.ASTConstants;
+import com.salesforce.graph.Schema;
+import com.salesforce.graph.build.CaseSafePropertyUtil;
+import com.salesforce.graph.vertex.MethodVertex;
+import com.salesforce.graph.vertex.SFVertexFactory;
+import com.salesforce.graph.vertex.UserClassVertex;
+import com.salesforce.metainfo.MetaInfoCollectorProvider;
+import com.salesforce.rules.AbstractRuleRunner;
+import java.util.*;
+import java.util.stream.Collectors;
+import org.apache.tinkerpop.gremlin.process.traversal.Order;
+import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.Scope;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.structure.Vertex;
+
+/**
+ * Util class for identifying and interacting with path entry points. A path entry point being
+ * defined as a point with which an external actor can interact, thus starting code execution.
+ */
+public final class PathEntryPointUtil {
+
+ /**
+ * Indicates whether a method vertex is a path entry point, e.g., a point where path analysis
+ * can begin.
+ */
+ public static boolean isPathEntryPoint(MethodVertex methodVertex) {
+ // Global methods are entry points.
+ if (methodVertex.getModifierNode().isGlobal()) {
+ return true;
+ }
+ // Methods that return PageReference objects are entry points.
+ if (methodVertex.getReturnType().equalsIgnoreCase(Schema.PAGE_REFERENCE)) {
+ return true;
+ }
+ // Certain annotations can designate a method as an entry point.
+ String[] entryPointAnnotations =
+ new String[] {
+ Schema.AURA_ENABLED,
+ Schema.NAMESPACE_ACCESSIBLE,
+ Schema.REMOTE_ACTION,
+ Schema.INVOCABLE_METHOD
+ };
+ for (String annotation : entryPointAnnotations) {
+ if (methodVertex.hasAnnotation(annotation)) {
+ return true;
+ }
+ }
+ // Exposed methods on VF controllers are entry points.
+ Set vfControllers =
+ MetaInfoCollectorProvider.getVisualForceHandler().getMetaInfoCollected().stream()
+ .map(String::toLowerCase)
+ .collect(Collectors.toSet());
+ if (vfControllers.contains(methodVertex.getDefiningType().toLowerCase())) {
+ return true;
+ }
+
+ // InboundEmailHandler methods are entry points.
+ // NOTE: This is a pretty cursory check and may struggle with nested inheritance. This isn't
+ // likely to happen, but if it does, we can make the check more robust.
+ Optional parentClass = methodVertex.getParentClass();
+ return parentClass.isPresent()
+ && parentClass.get().getInterfaceNames().stream()
+ .map(String::toLowerCase)
+ .collect(Collectors.toSet())
+ // Does the parent class implement InboundEmailHandler?
+ .contains(Schema.INBOUND_EMAIL_HANDLER.toLowerCase())
+ // Does the method return an InboundEmailResult?
+ && methodVertex.getReturnType().equalsIgnoreCase(Schema.INBOUND_EMAIL_RESULT)
+ // Is the method named handleInboundEmail?
+ && methodVertex.getName().equalsIgnoreCase(Schema.HANDLE_INBOUND_EMAIL);
+ }
+
+ /** Load all path entry points in the graph. */
+ static List getPathEntryPoints(GraphTraversalSource g) {
+ return getPathEntryPoints(g, new ArrayList<>());
+ }
+
+ /**
+ * Load all path entry points specified by the target objects. An empty list implicitly includes
+ * all files.
+ */
+ public static List getPathEntryPoints(
+ GraphTraversalSource g, List targets) {
+ // Sort the list of targets into full-file targets and method-level targets.
+ List fileLevelTargets =
+ targets.stream()
+ .filter(t -> t.getTargetMethods().isEmpty())
+ .map(AbstractRuleRunner.RuleRunnerTarget::getTargetFile)
+ .collect(Collectors.toList());
+ List methodLevelTargets =
+ targets.stream()
+ .filter(t -> !t.getTargetMethods().isEmpty())
+ .collect(Collectors.toList());
+
+ // Internally, we'll use a Set to preserve uniqueness.
+ Set methods = new HashSet<>();
+
+ // If there are any explicitly targeted files, we must process them. If there are no
+ // explicit targets of any kind,
+ // then all files are implicitly targeted.
+ if (!fileLevelTargets.isEmpty() || targets.isEmpty()) {
+ // Use the file-level targets to get aura-enabled methods...
+ methods.addAll(getAuraEnabledMethods(g, fileLevelTargets));
+ // ...and NamespaceAccessible methods...
+ methods.addAll(getNamespaceAccessibleMethods(g, fileLevelTargets));
+ // ...and RemoteAction methods...
+ methods.addAll(getRemoteActionMethods(g, fileLevelTargets));
+ // ...and InvocableMethod methods...
+ methods.addAll(getInvocableMethodMethods(g, fileLevelTargets));
+ // ...and PageReference methods...
+ methods.addAll(getPageReferenceMethods(g, fileLevelTargets));
+ // ...and global-exposed methods...
+ methods.addAll(getGlobalMethods(g, fileLevelTargets));
+ // ...and implementations of Messaging.InboundEmailHandler#handleInboundEmail...
+ methods.addAll(getInboundEmailHandlerMethods(g, fileLevelTargets));
+ // ...and exposed methods on VF controllers.
+ methods.addAll(getExposedControllerMethods(g, fileLevelTargets));
+ }
+
+ // Also, if there are any specifically targeted methods, they should be included.
+ if (!methodLevelTargets.isEmpty()) {
+ methods.addAll(MethodUtil.getTargetedMethods(g, methodLevelTargets));
+ }
+ // Turn the Set into a List so we can return it.
+ return new ArrayList<>(methods);
+ }
+
+ /**
+ * Returns non-test methods in the target files with an @AuraEnabled annotation. An empty list
+ * implicitly includes all files.
+ */
+ private static List getAuraEnabledMethods(
+ GraphTraversalSource g, List targetFiles) {
+ return getMethodsWithAnnotation(g, targetFiles, Schema.AURA_ENABLED);
+ }
+
+ /**
+ * Returns non-test methods in the target files with a @NamespaceAccessible annotation. An empty
+ * list implicitly includes all files.
+ */
+ private static List getNamespaceAccessibleMethods(
+ GraphTraversalSource g, List targetFiles) {
+ return getMethodsWithAnnotation(g, targetFiles, Schema.NAMESPACE_ACCESSIBLE);
+ }
+
+ /**
+ * Returns non-test methods in the target files with a @RemoteAction annotation. An empty list
+ * implicitly includes all files.
+ */
+ private static List getRemoteActionMethods(
+ GraphTraversalSource g, List targetFiles) {
+ return getMethodsWithAnnotation(g, targetFiles, Schema.REMOTE_ACTION);
+ }
+
+ /**
+ * Returns non-test methods in the target files with an @InvocableMethod annotation. An empty
+ * list implicitly includes all files.
+ */
+ private static List getInvocableMethodMethods(
+ GraphTraversalSource g, List targetFiles) {
+ return getMethodsWithAnnotation(g, targetFiles, Schema.INVOCABLE_METHOD);
+ }
+
+ static List getMethodsWithAnnotation(
+ GraphTraversalSource g, List targetFiles, String annotation) {
+ return SFVertexFactory.loadVertices(
+ g,
+ rootMethodTraversal(g, targetFiles)
+ .where(
+ out(Schema.CHILD)
+ .hasLabel(ASTConstants.NodeType.MODIFIER_NODE)
+ .out(Schema.CHILD)
+ .where(
+ CaseSafePropertyUtil.H.has(
+ ASTConstants.NodeType.ANNOTATION,
+ Schema.NAME,
+ annotation)))
+ .order(Scope.global)
+ .by(Schema.DEFINING_TYPE, Order.asc)
+ .by(Schema.NAME, Order.asc));
+ }
+
+ /**
+ * Returns non-test methods in the target files whose return type is a PageReference. An empty
+ * list implicitly includes all files.
+ */
+ static List getPageReferenceMethods(
+ GraphTraversalSource g, List targetFiles) {
+ return SFVertexFactory.loadVertices(
+ g,
+ rootMethodTraversal(g, targetFiles)
+ .where(
+ CaseSafePropertyUtil.H.has(
+ ASTConstants.NodeType.METHOD,
+ Schema.RETURN_TYPE,
+ Schema.PAGE_REFERENCE))
+ .order(Scope.global)
+ .by(Schema.DEFINING_TYPE, Order.asc)
+ .by(Schema.NAME, Order.asc));
+ }
+
+ private static GraphTraversal rootMethodTraversal(
+ GraphTraversalSource g, List targetFiles) {
+ // Only look at UserClass vertices. Not interested in Enums, Interfaces, or Triggers
+ final String[] labels = new String[] {ASTConstants.NodeType.USER_CLASS};
+ return TraversalUtil.fileRootTraversal(g, labels, targetFiles)
+ .not(has(Schema.IS_TEST, true))
+ .repeat(__.out(Schema.CHILD))
+ .until(__.hasLabel(ASTConstants.NodeType.METHOD))
+ .not(has(Schema.IS_TEST, true));
+ }
+
+ /**
+ * Returns non-test methods in the target files whose modifier scope is `global`. An empty list
+ * implicitly includes all files.
+ */
+ static List getGlobalMethods(GraphTraversalSource g, List targetFiles) {
+ // Get all methods in the target files.
+ return SFVertexFactory.loadVertices(
+ g,
+ rootMethodTraversal(g, targetFiles)
+ .filter(
+ __.and(
+ // If a method has at least one block statement, then it is
+ // definitely actually declared, as
+ // opposed to being an implicit method.
+ out(Schema.CHILD)
+ .hasLabel(ASTConstants.NodeType.BLOCK_STATEMENT)
+ .count()
+ .is(P.gte(1)),
+ // We only want global methods.
+ out(Schema.CHILD)
+ .hasLabel(ASTConstants.NodeType.MODIFIER_NODE)
+ .has(Schema.GLOBAL, true),
+ // Ignore any standard methods, otherwise will get a ton of
+ // extra results.
+ __.not(__.has(Schema.IS_STANDARD, true)))));
+ }
+
+ static List getInboundEmailHandlerMethods(
+ GraphTraversalSource g, List targetFiles) {
+ return SFVertexFactory.loadVertices(
+ g,
+ // Get any target class that implements the email handler interface.
+ TraversalUtil.traverseImplementationsOf(
+ g, targetFiles, Schema.INBOUND_EMAIL_HANDLER)
+ // Get every implementation of the handle email method.
+ .out(Schema.CHILD)
+ .where(
+ CaseSafePropertyUtil.H.has(
+ ASTConstants.NodeType.METHOD,
+ Schema.NAME,
+ Schema.HANDLE_INBOUND_EMAIL))
+ // Filter the results by return type and arity to limit the possibility of
+ // getting unnecessary results.
+ .where(
+ CaseSafePropertyUtil.H.has(
+ ASTConstants.NodeType.METHOD,
+ Schema.RETURN_TYPE,
+ Schema.INBOUND_EMAIL_RESULT))
+ .has(Schema.ARITY, 2));
+ }
+
+ /**
+ * Returns all non-test public- and global-scoped methods in controllers referenced by
+ * VisualForce pages, filtered by target file list. An empty list implicitly includes all files.
+ *
+ * @param g
+ * @param targetFiles
+ * @return
+ */
+ static List getExposedControllerMethods(
+ GraphTraversalSource g, List targetFiles) {
+ Set referencedVfControllers =
+ MetaInfoCollectorProvider.getVisualForceHandler().getMetaInfoCollected();
+ // If none of the VF files referenced an Apex class, we can just return an empty list.
+ if (referencedVfControllers.isEmpty()) {
+ return new ArrayList<>();
+ }
+ List allControllerMethods =
+ SFVertexFactory.loadVertices(
+ g,
+ TraversalUtil.fileRootTraversal(g, targetFiles)
+ // Only outer classes can be VF controllers, so we should restrict
+ // our query to UserClasses.
+ .where(
+ CaseSafePropertyUtil.H.hasWithin(
+ ASTConstants.NodeType.USER_CLASS,
+ Schema.DEFINING_TYPE,
+ referencedVfControllers))
+ .repeat(__.out(Schema.CHILD))
+ .until(__.hasLabel(ASTConstants.NodeType.METHOD))
+ .not(has(Schema.IS_TEST, true))
+ // We want to ignore constructors.
+ .where(
+ __.not(
+ CaseSafePropertyUtil.H.hasWithin(
+ ASTConstants.NodeType.METHOD,
+ Schema.NAME,
+ Schema.INSTANCE_CONSTRUCTOR_CANONICAL_NAME,
+ Schema
+ .STATIC_CONSTRUCTOR_CANONICAL_NAME))));
+ // Gremlin isn't sophisticated enough to perform this kind of filtering in the actual query.
+ // So we'll just do it
+ // manually here.
+ return allControllerMethods.stream()
+ .filter(
+ methodVertex ->
+ methodVertex.getModifierNode().isPublic()
+ || methodVertex.getModifierNode().isGlobal())
+ .collect(Collectors.toList());
+ }
+
+ private PathEntryPointUtil() {}
+}
diff --git a/sfge/src/main/java/com/salesforce/graph/ops/TypeableUtil.java b/sfge/src/main/java/com/salesforce/graph/ops/TypeableUtil.java
index 076a87f0a..5f6374e7a 100644
--- a/sfge/src/main/java/com/salesforce/graph/ops/TypeableUtil.java
+++ b/sfge/src/main/java/com/salesforce/graph/ops/TypeableUtil.java
@@ -22,10 +22,14 @@ public final class TypeableUtil {
private static final Logger LOGGER = LogManager.getLogger(TypeableUtil.class);
- private static final String LIST_PATTERN_STR = "List<(.*)>";
+ private static final String LIST_PATTERN_STR = "list<\\s*([^\\s]*)\\s*>";
private static final Pattern LIST_PATTERN =
Pattern.compile(LIST_PATTERN_STR, Pattern.CASE_INSENSITIVE);
+ private static final String SET_PATTERN_STR = "set<\\s*([^\\s]*)\\s*>";
+ private static final Pattern SET_PATTERN =
+ Pattern.compile(SET_PATTERN_STR, Pattern.CASE_INSENSITIVE);
+
private TypeableUtil() {}
/**
@@ -111,9 +115,25 @@ private static OrderedTreeSet getListTypeHierarchy(String definingType) {
* string
*/
public static Optional getListSubType(String definingType) {
- final Matcher listPatternMatcher = LIST_PATTERN.matcher(definingType);
- if (listPatternMatcher.find()) {
- return Optional.of(listPatternMatcher.group(1));
+ return getSubType(LIST_PATTERN, definingType, 1);
+ }
+
+ public static Optional getSetSubType(String definingType) {
+ return getSubType(SET_PATTERN, definingType, 1);
+ }
+
+ private static Optional getSubType(
+ Pattern subTypePattern, String definingType, int groupCountExpected) {
+ final Matcher subTypeMatcher = subTypePattern.matcher(definingType);
+
+ if (subTypeMatcher.find()) {
+ if (subTypeMatcher.groupCount() != groupCountExpected) {
+ throw new UnexpectedException(
+ String.format(
+ "Unexpected number of subtypes in %s. Expected: %d, Actual: %d",
+ definingType, groupCountExpected, subTypeMatcher.groupCount()));
+ }
+ return Optional.of(subTypeMatcher.group(1));
}
return Optional.empty();
}
diff --git a/sfge/src/main/java/com/salesforce/graph/symbols/ClassStaticScope.java b/sfge/src/main/java/com/salesforce/graph/symbols/ClassStaticScope.java
index df046f8bf..8187bcd53 100644
--- a/sfge/src/main/java/com/salesforce/graph/symbols/ClassStaticScope.java
+++ b/sfge/src/main/java/com/salesforce/graph/symbols/ClassStaticScope.java
@@ -96,7 +96,7 @@ private static List getFieldDeclarations(
g.V(userClass.getId())
.out(Schema.CHILD)
.hasLabel(NodeType.METHOD)
- .has(Schema.NAME, MethodUtil.STATIC_CONSTRUCTOR_CANONICAL_NAME)
+ .has(Schema.NAME, Schema.STATIC_CONSTRUCTOR_CANONICAL_NAME)
// This is a static initializer block, not a constructor
.has(Schema.CONSTRUCTOR, false)
.has(Schema.ARITY, 0)
diff --git a/sfge/src/main/java/com/salesforce/graph/symbols/PathScopeVisitor.java b/sfge/src/main/java/com/salesforce/graph/symbols/PathScopeVisitor.java
index 15007bf5e..1c53347bf 100644
--- a/sfge/src/main/java/com/salesforce/graph/symbols/PathScopeVisitor.java
+++ b/sfge/src/main/java/com/salesforce/graph/symbols/PathScopeVisitor.java
@@ -2,24 +2,16 @@
import com.salesforce.apex.jorje.ASTConstants;
import com.salesforce.collections.CollectionUtil;
+import com.salesforce.config.UserFacingMessages;
import com.salesforce.exception.TodoException;
import com.salesforce.exception.UnexpectedException;
+import com.salesforce.exception.UserActionException;
import com.salesforce.graph.ops.ApexClassUtil;
import com.salesforce.graph.ops.ApexStandardLibraryUtil;
import com.salesforce.graph.ops.ApexValueUtil;
import com.salesforce.graph.ops.CloneUtil;
import com.salesforce.graph.ops.MethodUtil;
-import com.salesforce.graph.symbols.apex.ApexClassInstanceValue;
-import com.salesforce.graph.symbols.apex.ApexForLoopValue;
-import com.salesforce.graph.symbols.apex.ApexListValue;
-import com.salesforce.graph.symbols.apex.ApexSingleValue;
-import com.salesforce.graph.symbols.apex.ApexSoqlValue;
-import com.salesforce.graph.symbols.apex.ApexStandardValue;
-import com.salesforce.graph.symbols.apex.ApexStringValue;
-import com.salesforce.graph.symbols.apex.ApexValue;
-import com.salesforce.graph.symbols.apex.ApexValueBuilder;
-import com.salesforce.graph.symbols.apex.ComplexAssignable;
-import com.salesforce.graph.symbols.apex.ValueStatus;
+import com.salesforce.graph.symbols.apex.*;
import com.salesforce.graph.vertex.ArrayLoadExpressionVertex;
import com.salesforce.graph.vertex.AssignmentExpressionVertex;
import com.salesforce.graph.vertex.BaseSFVertex;
@@ -1600,7 +1592,11 @@ public boolean visit(VariableDeclarationVertex vertex) {
final String key = vertex.getName();
if (apexValueStack.peek().containsKey(key)) {
// The variable was defined multiple times
- throw new UnexpectedException(vertex);
+ throw new UserActionException(
+ UserFacingMessages.VARIABLE_DECLARED_MULTIPLE_TIMES,
+ vertex.getFileName(),
+ vertex.getDefiningType(),
+ vertex.getBeginLine());
}
ChainedVertex rhs = vertex.getRhs().orElse(null);
@@ -1654,6 +1650,7 @@ public boolean visit(VariableExpressionVertex.ForLoop vertex) {
trackVisited(vertex);
String key = vertex.getName();
ApexValue> apexValue = getApexValue(key).get();
+
ApexValue> newValue =
ApexValueBuilder.get(this)
.declarationVertex(apexValue.getTypeVertex().get())
diff --git a/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexForLoopValue.java b/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexForLoopValue.java
index f88390866..66127d637 100644
--- a/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexForLoopValue.java
+++ b/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexForLoopValue.java
@@ -7,12 +7,7 @@
import com.salesforce.graph.symbols.DeepCloneContextProvider;
import com.salesforce.graph.symbols.ScopeUtil;
import com.salesforce.graph.symbols.SymbolProvider;
-import com.salesforce.graph.vertex.ChainedVertex;
-import com.salesforce.graph.vertex.InvocableVertex;
-import com.salesforce.graph.vertex.MethodCallExpressionVertex;
-import com.salesforce.graph.vertex.NewListInitExpressionVertex;
-import com.salesforce.graph.vertex.NewListLiteralExpressionVertex;
-import com.salesforce.graph.vertex.VariableExpressionVertex;
+import com.salesforce.graph.vertex.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -31,19 +26,23 @@ public final class ApexForLoopValue extends ApexPropertiesValue();
setValue(value);
if (valueVertex != null
&& !(valueVertex instanceof VariableExpressionVertex.ForLoop)
&& !(valueVertex instanceof NewListInitExpressionVertex)
- && !(valueVertex instanceof NewListLiteralExpressionVertex)) {
+ && !(valueVertex instanceof NewListLiteralExpressionVertex)
+ && !(valueVertex instanceof NewSetInitExpressionVertex)
+ && !(valueVertex instanceof NewSetLiteralExpressionVertex)) {
throw new UnexpectedException(valueVertex);
}
}
@@ -83,30 +82,36 @@ public List> getForLoopValues() {
private void setValue(@Nullable ChainedVertex valueVertex, SymbolProvider symbolProvider) {
this.items.clear();
- ApexListValue apexListValue = null;
+ ApexIterableCollectionValue collectionValue = null;
if (valueVertex instanceof VariableExpressionVertex.ForLoop) {
ChainedVertex forLoopValues =
((VariableExpressionVertex.ForLoop) valueVertex).getForLoopValues();
ApexValue> apexValue =
ScopeUtil.resolveToApexValue(symbolProvider, forLoopValues).orElse(null);
- if (apexValue instanceof ApexListValue) {
- apexListValue = (ApexListValue) apexValue;
+ if (apexValue instanceof ApexIterableCollectionValue) {
+ collectionValue = (ApexIterableCollectionValue) apexValue;
}
} else if (valueVertex instanceof NewListLiteralExpressionVertex) {
- apexListValue =
+ collectionValue =
ApexValueBuilder.get(symbolProvider).valueVertex(valueVertex).buildList();
+ } else if (valueVertex instanceof NewSetLiteralExpressionVertex) {
+ collectionValue =
+ ApexValueBuilder.get(symbolProvider).valueVertex(valueVertex).buildSet();
}
- if (apexListValue != null) {
- setValue(apexListValue);
+ if (collectionValue != null) {
+ setValue(collectionValue);
}
}
- private void setValue(ApexListValue apexListValue) {
+ private void setValue(ApexIterableCollectionValue collectionValue) {
// Pass on sanitization information
- AbstractSanitizableValue.copySanitization(apexListValue, this);
+ if (collectionValue instanceof AbstractSanitizableValue) {
+ AbstractSanitizableValue.copySanitization(
+ (AbstractSanitizableValue) collectionValue, this);
+ }
// Add items in the list
- for (ApexValue> item : apexListValue.getValues()) {
+ for (ApexValue> item : collectionValue.getValues()) {
items.add(item);
}
}
@@ -125,10 +130,21 @@ public Optional> apply(MethodCallExpressionVertex vertex, SymbolPro
// This will check for null access and throw an exception if this would never
// continue in production
apexValue.checkForNullAccess(vertex, symbols);
- ApexValue> applied = apexValue.apply(vertex, symbols).orElse(null);
- final ApexValue> valueToAdd = applied != null ? applied : apexValue.deepClone();
-
- result.items.add(valueToAdd);
+ Optional> optApplied = apexValue.apply(vertex, symbols);
+ ApexValue> valueToAdd;
+ if (optApplied.isPresent()) {
+ valueToAdd = optApplied.get();
+ } else {
+ // TODO: path expander needs to expand on this method call and return a value
+ // For now, we clone the same apexValue as a temporary bandage
+ // to handle cases where we don't know what value a method call returns.
+ // NOTE: The returned value is incorrect until we fix this.
+ valueToAdd = apexValue.deepClone();
+ }
+
+ if (valueToAdd != null) {
+ result.items.add(valueToAdd);
+ }
}
}
diff --git a/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexIterableCollectionValue.java b/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexIterableCollectionValue.java
new file mode 100644
index 000000000..03f482bd7
--- /dev/null
+++ b/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexIterableCollectionValue.java
@@ -0,0 +1,19 @@
+package com.salesforce.graph.symbols.apex;
+
+import com.salesforce.graph.vertex.Typeable;
+import java.util.Collection;
+import java.util.Optional;
+
+/** Represents ApexValues that hold an iterable collection of values. */
+public interface ApexIterableCollectionValue {
+ /**
+ * @return values that the collection holds
+ */
+ Collection> getValues();
+
+ /**
+ * @return SubType of the collection value. For example, List would return a String
+ * Typeable
+ */
+ Optional getSubType();
+}
diff --git a/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexListValue.java b/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexListValue.java
index ce4ce92d2..e1b09275a 100644
--- a/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexListValue.java
+++ b/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexListValue.java
@@ -9,6 +9,7 @@
import com.salesforce.graph.DeepCloneable;
import com.salesforce.graph.ops.ApexValueUtil;
import com.salesforce.graph.ops.CloneUtil;
+import com.salesforce.graph.ops.TypeableUtil;
import com.salesforce.graph.symbols.ScopeUtil;
import com.salesforce.graph.symbols.SymbolProvider;
import com.salesforce.graph.vertex.ChainedVertex;
@@ -24,7 +25,6 @@
import java.util.Optional;
import java.util.TreeMap;
import java.util.function.Function;
-import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.apache.commons.lang3.tuple.Pair;
@@ -36,7 +36,7 @@
* https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_methods_system_list.htm
*/
public final class ApexListValue extends AbstractSanitizableValue
- implements DeepCloneable {
+ implements DeepCloneable, ApexIterableCollectionValue {
private static final Logger LOGGER = LogManager.getLogger(ApexListValue.class);
public static final String METHOD_ADD = "add";
@@ -100,6 +100,7 @@ public U accept(ApexValueVisitor visitor) {
return visitor.visit(this);
}
+ @Override
public List> getValues() {
return Collections.unmodifiableList(values);
}
@@ -361,19 +362,20 @@ private static Optional identifyType(@Nullable Typeable typeable) {
}
final String canonicalType = typeable.getCanonicalType();
- final Matcher matcher = TYPE_PATTERN.matcher(canonicalType);
- if (matcher.find()) {
- if (matcher.groupCount() != 1) {
- throw new UnexpectedException(
- "Expected to find only one Type in list declaration: " + typeable);
- }
- return Optional.of(SyntheticTypedVertex.get(matcher.group(1)));
+ Optional optSubType = TypeableUtil.getListSubType(canonicalType);
+ if (optSubType.isPresent()) {
+ return Optional.of(SyntheticTypedVertex.get(optSubType.get()));
}
// If we don't find a match, we are still okay.
return Optional.empty();
}
+ @Override
+ public Optional getSubType() {
+ return Optional.ofNullable(this.listType);
+ }
+
@Override
public void markSanitized(
MethodBasedSanitization.SanitizerMechanism sanitizerMechanism,
diff --git a/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexSetValue.java b/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexSetValue.java
index a2c020649..fee616fe3 100644
--- a/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexSetValue.java
+++ b/sfge/src/main/java/com/salesforce/graph/symbols/apex/ApexSetValue.java
@@ -8,13 +8,10 @@
import com.salesforce.graph.DeepCloneable;
import com.salesforce.graph.ops.ApexValueUtil;
import com.salesforce.graph.ops.CloneUtil;
+import com.salesforce.graph.ops.TypeableUtil;
import com.salesforce.graph.symbols.ScopeUtil;
import com.salesforce.graph.symbols.SymbolProvider;
-import com.salesforce.graph.vertex.ChainedVertex;
-import com.salesforce.graph.vertex.InvocableVertex;
-import com.salesforce.graph.vertex.MethodCallExpressionVertex;
-import com.salesforce.graph.vertex.NewSetInitExpressionVertex;
-import com.salesforce.graph.vertex.NewSetLiteralExpressionVertex;
+import com.salesforce.graph.vertex.*;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -30,7 +27,8 @@
// constructor
// invokes ApexListValue.getValues(). Since ApexListValue is final, the method can't be overriden.
// Hence the violation is incorrect.
-public class ApexSetValue extends ApexValue implements DeepCloneable {
+public class ApexSetValue extends AbstractSanitizableValue
+ implements DeepCloneable, ApexIterableCollectionValue {
private static final String METHOD_ADD = "add";
private static final String METHOD_CLEAR = "clear";
private static final String METHOD_CLONE = "clone";
@@ -39,17 +37,21 @@ public class ApexSetValue extends ApexValue implements DeepCloneab
protected static final String METHOD_SIZE = "size";
private final NonNullHashSet> values;
+ private final Typeable type;
public ApexSetValue(ApexValueBuilder builder) {
super(builder);
this.values = CollectionUtil.newNonNullHashSet();
setValue(builder.getValueVertex(), builder.getSymbolProvider());
+ final Typeable typeable = builder.getMostSpecificTypedVertex().orElse(null);
+ this.type = identifyType(typeable).orElse(null);
}
/** DeepCloneable#deepClone constructor */
private ApexSetValue(ApexSetValue other) {
super(other, other.getReturnedFrom().orElse(null), other.getInvocable().orElse(null));
this.values = CloneUtil.cloneNonNullHashSet(other.values);
+ this.type = other.type;
}
/**
@@ -64,6 +66,7 @@ private ApexSetValue(
@Nullable InvocableVertex invocable) {
super(other, returnedFrom, invocable);
this.values = values;
+ this.type = other.type;
}
@Override
@@ -188,6 +191,7 @@ public void add(ApexValue> value) {
this.values.add(value);
}
+ @Override
public List> getValues() {
return Collections.unmodifiableList(values.stream().collect(Collectors.toList()));
}
@@ -196,4 +200,24 @@ public List> getValues() {
public Optional getDefiningType() {
return Optional.empty();
}
+
+ // TODO: consolidate logic with ApexListValue
+ private static Optional identifyType(@Nullable Typeable typeable) {
+ if (typeable == null) {
+ return Optional.empty();
+ }
+
+ final Optional optSubType = TypeableUtil.getSetSubType(typeable.getCanonicalType());
+ if (optSubType.isPresent()) {
+ return Optional.of(SyntheticTypedVertex.get(optSubType.get()));
+ }
+
+ // If we don't find a match, we are still okay.
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional getSubType() {
+ return Optional.ofNullable(this.type);
+ }
}
diff --git a/sfge/src/main/java/com/salesforce/graph/symbols/apex/schema/DescribeFieldResult.java b/sfge/src/main/java/com/salesforce/graph/symbols/apex/schema/DescribeFieldResult.java
index a049eff89..b93284acc 100644
--- a/sfge/src/main/java/com/salesforce/graph/symbols/apex/schema/DescribeFieldResult.java
+++ b/sfge/src/main/java/com/salesforce/graph/symbols/apex/schema/DescribeFieldResult.java
@@ -5,13 +5,7 @@
import com.salesforce.graph.ops.ApexValueUtil;
import com.salesforce.graph.ops.CloneUtil;
import com.salesforce.graph.symbols.SymbolProvider;
-import com.salesforce.graph.symbols.apex.ApexForLoopValue;
-import com.salesforce.graph.symbols.apex.ApexSingleValue;
-import com.salesforce.graph.symbols.apex.ApexStandardValue;
-import com.salesforce.graph.symbols.apex.ApexStringValue;
-import com.salesforce.graph.symbols.apex.ApexValue;
-import com.salesforce.graph.symbols.apex.ApexValueBuilder;
-import com.salesforce.graph.symbols.apex.ApexValueVisitor;
+import com.salesforce.graph.symbols.apex.*;
import com.salesforce.graph.vertex.InvocableWithParametersVertex;
import com.salesforce.graph.vertex.MethodCallExpressionVertex;
import com.salesforce.graph.vertex.MethodVertex;
@@ -121,7 +115,16 @@ public Optional> getFieldName() {
@Override
public Optional> apply(MethodCallExpressionVertex vertex, SymbolProvider symbols) {
- return Optional.empty();
+ final String methodName = vertex.getMethodName();
+ ApexValueBuilder builder = ApexValueBuilder.get(symbols).returnedFrom(this, vertex);
+
+ ApexValue> apexValue;
+ if (SystemNames.DML_FIELD_ACCESS_METHODS.contains(methodName)) {
+ apexValue = builder.withStatus(ValueStatus.INDETERMINANT).buildBoolean();
+ } else {
+ apexValue = _applyMethod(vertex, builder, methodName).orElse(null);
+ }
+ return Optional.ofNullable(apexValue);
}
@Override
@@ -136,6 +139,20 @@ public Optional> executeMethod(
.methodVertex(method);
String methodName = method.getName();
+ Optional> optApexValue =
+ _applyMethod(invocableExpression, builder, methodName);
+
+ if (!optApexValue.isPresent()) {
+ optApexValue = Optional.of(ApexValueUtil.synthesizeReturnedValue(builder, method));
+ }
+
+ return optApexValue;
+ }
+
+ private Optional> _applyMethod(
+ InvocableWithParametersVertex invocableExpression,
+ ApexValueBuilder builder,
+ String methodName) {
if (METHOD_GET_NAME.equalsIgnoreCase(methodName)) {
if (fieldName != null && fieldName.isDeterminant()) {
if (fieldName instanceof ApexStringValue) {
@@ -163,9 +180,8 @@ public Optional> executeMethod(
builder.buildSObjectField(
describeSObjectResult.getSObjectType().get(), fieldName));
}
- } else {
- return Optional.of(ApexValueUtil.synthesizeReturnedValue(builder, method));
}
+ return Optional.empty();
}
@Override
diff --git a/sfge/src/main/java/com/salesforce/graph/symbols/apex/schema/DescribeSObjectResult.java b/sfge/src/main/java/com/salesforce/graph/symbols/apex/schema/DescribeSObjectResult.java
index 68213a678..d3fe85d71 100644
--- a/sfge/src/main/java/com/salesforce/graph/symbols/apex/schema/DescribeSObjectResult.java
+++ b/sfge/src/main/java/com/salesforce/graph/symbols/apex/schema/DescribeSObjectResult.java
@@ -6,12 +6,7 @@
import com.salesforce.graph.ops.ApexValueUtil;
import com.salesforce.graph.ops.CloneUtil;
import com.salesforce.graph.symbols.SymbolProvider;
-import com.salesforce.graph.symbols.apex.ApexStandardValue;
-import com.salesforce.graph.symbols.apex.ApexStringValue;
-import com.salesforce.graph.symbols.apex.ApexValue;
-import com.salesforce.graph.symbols.apex.ApexValueBuilder;
-import com.salesforce.graph.symbols.apex.ApexValueVisitor;
-import com.salesforce.graph.symbols.apex.ValueStatus;
+import com.salesforce.graph.symbols.apex.*;
import com.salesforce.graph.vertex.InvocableWithParametersVertex;
import com.salesforce.graph.vertex.MethodCallExpressionVertex;
import com.salesforce.graph.vertex.MethodVertex;
@@ -79,34 +74,40 @@ public Optional> apply(MethodCallExpressionVertex vertex, SymbolPro
String methodName = vertex.getMethodName();
List chainedNames = vertex.getChainedNames();
+ ApexValue> apexValue = null;
+
if (METHOD_GET_MAP.equalsIgnoreCase(methodName) && !chainedNames.isEmpty()) {
String mapType = chainedNames.get(chainedNames.size() - 1);
// objectDescribe.fields.getMap()
if (FIELDS.equalsIgnoreCase(mapType)) {
validateParameterSize(vertex, 0);
if (sObjectType != null) {
- return Optional.of(builder.buildApexFieldDescribeMapValue(sObjectType));
+ apexValue = builder.buildApexFieldDescribeMapValue(sObjectType);
} else {
- return Optional.of(
+ apexValue =
builder.buildApexFieldDescribeMapValue(
- builder.deepClone().buildSObjectType()));
+ builder.deepClone().buildSObjectType());
}
// objectDescribe.fieldSets.getMap()
} else if (FIELD_SETS.equalsIgnoreCase(mapType)) {
validateParameterSize(vertex, 0);
if (sObjectType != null) {
- return Optional.of(builder.buildApexFieldSetDescribeMapValue(sObjectType));
+ apexValue = builder.buildApexFieldSetDescribeMapValue(sObjectType);
} else {
- return Optional.of(
+ apexValue =
builder.buildApexFieldSetDescribeMapValue(
- builder.deepClone().buildSObjectType()));
+ builder.deepClone().buildSObjectType());
}
} else {
throw new UnexpectedException(vertex);
}
}
- return Optional.empty();
+ if (apexValue == null) {
+ apexValue = _applyMethod(vertex, builder, methodName).orElse(null);
+ }
+
+ return Optional.ofNullable(apexValue);
}
@Override
@@ -120,6 +121,19 @@ public Optional> executeMethod(
.methodVertex(method);
String methodName = method.getName();
+ Optional> optApexValue =
+ _applyMethod(invocableExpression, builder, methodName);
+
+ if (!optApexValue.isPresent()) {
+ optApexValue = Optional.of(ApexValueUtil.synthesizeReturnedValue(builder, method));
+ }
+ return optApexValue;
+ }
+
+ private Optional> _applyMethod(
+ InvocableWithParametersVertex invocableExpression,
+ ApexValueBuilder builder,
+ String methodName) {
if (METHOD_GET_NAME.equalsIgnoreCase(methodName)) {
if (sObjectType != null && ApexValueUtil.isDeterminant(sObjectType.getType())) {
ApexValue> value = sObjectType.getType().get();
@@ -153,9 +167,10 @@ public Optional> executeMethod(
} else {
return Optional.of(builder.buildSObjectType());
}
- } else {
- return Optional.of(ApexValueUtil.synthesizeReturnedValue(builder, method));
+ } else if (SystemNames.DML_OBJECT_ACCESS_METHODS.contains(methodName)) {
+ return Optional.of(builder.withStatus(ValueStatus.INDETERMINANT).buildBoolean());
}
+ return Optional.empty();
}
@Override
diff --git a/sfge/src/main/java/com/salesforce/graph/symbols/apex/schema/FieldSet.java b/sfge/src/main/java/com/salesforce/graph/symbols/apex/schema/FieldSet.java
index b0230d92f..d4950761d 100644
--- a/sfge/src/main/java/com/salesforce/graph/symbols/apex/schema/FieldSet.java
+++ b/sfge/src/main/java/com/salesforce/graph/symbols/apex/schema/FieldSet.java
@@ -16,6 +16,7 @@
import com.salesforce.graph.vertex.MethodCallExpressionVertex;
import com.salesforce.graph.vertex.MethodVertex;
import com.salesforce.graph.vertex.SyntheticTypedVertex;
+import java.util.Objects;
import java.util.Optional;
public final class FieldSet extends ApexStandardValue