FormulaEval.FormulaBuilder chain calls build() without withReturnType
ID |
apex.correctness.formulabuilder_configuration |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Api Usage |
Language |
Apex |
Description
Reports a Formula.builder() or FormulaEval.FormulaBuilder.builder() chain that reaches build() without calling withReturnType(…) anywhere in the same chain. Only the chained form is reported; a builder held in a local variable and configured over several statements is not seen.
Rationale
build() validates the configuration it was given and throws FormulaEval.FormulaValidationException when it is incomplete, so a chain missing the return type compiles, deploys and passes every test that never reaches the line - and then throws the first time the formula is actually evaluated. The return type is what tells the evaluator how to coerce the result before handing it back, so leaving it out is not a defaulting question: there is no value to hand to evaluate. Dynamic formulas are usually built from configuration rather than from literals, which means the broken path is often one record type or one custom setting away from the one the test class exercised.
The following code illustrates the pattern detected by this rule:
public class DiscountFormula {
public Object evaluateForAccount(Account account, String expression) {
// FLAGGED: FormulaEval.FormulaBuilder chain calls build() without withReturnType
FormulaEval.FormulaInstance instance = Formula.builder()
.withType(Account.SObjectType)
.withFormula(expression)
.build();
return instance.evaluate(account);
}
public Object evaluateForApexObject(Quote quote, String expression) {
// FLAGGED: FormulaEval.FormulaBuilder chain calls build() without withReturnType
FormulaEval.FormulaInstance instance = FormulaEval.FormulaBuilder.builder()
.withType(Quote.class)
.withFormula(expression)
.build();
return instance.evaluate(quote);
}
public Object evaluateConfigured(Account account, String expression) {
FormulaEval.FormulaInstance instance = Formula.builder()
.withType(Account.SObjectType)
.withReturnType(FormulaEval.FormulaReturnType.BOOLEAN)
.withFormula(expression)
.build();
return instance.evaluate(account);
}
public Object evaluateWithGlobals(Account account, String expression) {
FormulaEval.FormulaInstance instance = FormulaEval.FormulaBuilder.builder()
.withType(Account.SObjectType)
.withReturnType(FormulaEval.FormulaReturnType.STRING)
.withGlobalVariables(new List<FormulaEval.FormulaGlobal>{ FormulaEval.FormulaGlobal.PROFILE })
.withFormula(expression)
.build();
return instance.evaluate(account);
}
public QuoteLine buildLine(Decimal amount) {
return QuoteLine.builder()
.withAmount(amount)
.build();
}
}
Remediation
Complete the chain before build(): withType(…) for the object or Apex class the formula is evaluated against, withFormula(…) for the expression text, and withReturnType(FormulaEval.FormulaReturnType….) for the type evaluate should return. Wrap the build() call in a try that catches FormulaEval.FormulaValidationException whenever the formula text comes from configuration, so a bad expression reports itself instead of failing the transaction.