forcedotcom / forcedotcom/code-analyzer
[BUG][code-analyzer] sfge: the ?? null-coalescing operator is unmodelled — four distinct crashes, one of which aborts the entire scan
- Dominant language
- TypeScript
- Stars
- 240
- Forks
- 52
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 5
Description
### Have you tried to resolve this issue yourself first?
- [x] I confirm I have gone through the above steps and still have an issue to report.
### Bug Description
**Engine:** `sfge` (Salesforce Graph Engine) · **Rule:** `ApexFlsViolation` (DevPreview) · **Selector:** `--rule-selector sfge`
The Apex null-coalescing operator `??` (API 60 / Spring '24) parses, but the vertex it produces in the sfge graph has **zero children** — the operands are never attached. Four different consumers then fail, depending on where the operator sits. I believe all four are one fix.
#1813 and #1967 already report one of these shapes (`for (X x : list ?? new List())`). The other three are unreported, and one of them is much more severe than the for-loop case.
**A. `??` in a condition** — `UnexpectedException: []` at `BooleanExpressionVertex.java:25`, whose check is `if (children.size() != 2) throw new UnexpectedException(children)`. The payload prints as `[]` — the node has no operands at all.
```apex
if ((a ?? false) && (b ?? false)) { insert new Account(Name = 'x'); }
```
**B. `??` as a method argument** — `UnexpectedException` at `ApexValue.java:610` via `ApexMapValue.java:218`, which asserts `put` has exactly 2 parameters. It *is* a 2-argument call in source, so the mis-modelled node changes the parameter count the engine sees.
```apex
Map record = new Map();
record.put('subField', maybeNull ?? '');
```
**C. `??` as a map-literal value** — `IndexOutOfBoundsException` at `MapEntryNodeVertex.java:38` (`getChildren().get(1)` on an entry with one child).
```apex
Map row = new Map{ 'id' => '001', 'status' => status ?? 'N/A' };
```
**D. `??` as a for-loop collection** (the severe one, = #1813/#1967) — fails at **graph build**, so it does not abandon one entry point, it **aborts the entire scan**: a single Sev-1 `UnexpectedEngineError` and zero analysis of every class in the workspace. I confirmed this by putting a second, unrelated, perfectly analysable class alongside it — it also produced no findings.
```apex
for (Account a : aList ?? new List()) { a.Name = 'x'; }
```
For calibration, `??` does **not** crash in a plain assignment (`String v = s ?? 'x';`) or a return (`return a ?? 'default';`). It only crashes where the missing children are actually walked, which is probably why this has looked like a narrow for-loop problem.
### Output / Logs
```shell
# A — ?? in a condition
UnexpectedException: null: com.salesforce.graph.vertex.SFVertexFactory.load(SFVertexFactory.java:245); ...
# the informative cause appears only in the sfge log, not in the violation:
Caused by: com.salesforce.exception.UnexpectedException: []
at com.salesforce.graph.vertex.BooleanExpressionVertex.(BooleanExpressionVertex.java:25)
at com.salesforce.graph.vertex.BooleanExpressionVertex.(BooleanExpressionVertex.java:18)
# B — ?? as a method argument
UnexpectedException: MethodCallExpressionVertex{fullMethodName=record.put, ... MethodName=put}:
com.salesforce.graph.symbols.apex.ApexValue.validateParameterSize(ApexValue.java:610);
com.salesforce.graph.symbols.apex.ApexMapValue.apply(ApexMapValue.java:218); ...
# C — ?? as a map-literal value
java.lang.IndexOutOfBoundsException: Index 1 out of bounds for length 1
at java.base/java.util.Collections$UnmodifiableList.get(Collections.java:1310)
at com.salesforce.graph.vertex.MapEntryNodeVertex.getValue(MapEntryNodeVertex.java:38)
at com.salesforce.graph.symbols.apex.ApexMapValue.setValue(ApexMapValue.java:133)
at com.salesforce.graph.symbols.apex.ApexMapValue.(ApexMapValue.java:47)
at com.salesforce.graph.symbols.apex.ApexValueBuilder.buildMap(ApexValueBuilder.java:253)
# D — ?? as a for-loop collection (aborts the whole run)
ERROR Sfge:159 - Unexpected exception while loading graph
com.salesforce.exception.UnexpectedException: vp[FirstChild->true], vp[BeginLine->4], ...
at com.salesforce.graph.build.GremlinUtil.getChildren(GremlinUtil.java:60)
at com.salesforce.graph.build.MethodPathBuilderVisitor.visitForEachStatement(MethodPathBuilderVisitor.java:235)
at com.salesforce.graph.build.MethodPathBuilderVisitor._visit(MethodPathBuilderVisitor.java:130)
at com.salesforce.graph.build.AbstractApexVertexBuilder.afterInsert(AbstractApexVertexBuilder.java:169)
at com.salesforce.graph.ops.GraphUtil.loadSourceFilesAndFolders(GraphUtil.java:170)
```
### Steps To Reproduce
1. Create an empty SFDX project (`sfdx-project.json` with a single `force-app` package directory).
2. Add these four classes under `force-app/main/default/classes/`, each with a standard `.cls-meta.xml` (apiVersion 62.0). **Put class D in a separate workspace from A-C** — it aborts the whole run and will mask the other three.
```apex
// A
public with sharing class CoalesceInCondition {
@AuraEnabled
public static void run(Boolean a, Boolean b) {
if ((a ?? false) && (b ?? false)) { insert new Account(Name = 'x'); }
}
}
```
```apex
// B
public with sharing class CoalesceInPutArg {
@AuraEnabled
public static void run(String maybeNull) {
Map record = new Map();
record.put('subField', maybeNull ?? '');
insert new Account(Name = record.get('subField'));
}
}
```
```apex
// C
public with sharing class CoalesceInMapLiteral {
@AuraEnabled
public static void run(String status) {
Map row = new Map{
'id' => '001',
'status' => status ?? 'N/A'
};
insert new Account(Name = String.valueOf(row.get('id')));
}
}
```
```apex
// D — run this one in its own workspace
public with sharing class CoalesceInForLoop {
@AuraEnabled
public static void run(List aList) {
for (Account a : aList ?? new List()) { a.Name = 'x'; }
update aList;
}
}
```
3. Add `code-analyzer.yml`:
```yaml
engines:
sfge:
java_thread_timeout: 900000
java_thread_count: 4
```
4. Run `sf code-analyzer run --rule-selector sfge --workspace . --config-file code-analyzer.yml`.
5. A, B and C each report an `InternalExecutionError` and yield no findings for that entry point. D returns a single Sev-1 `UnexpectedEngineError` and analyses nothing at all.
### Expected Behavior
`a ?? b` should build a node with both operands attached, so path evaluation can treat it like the equivalent ternary (`a != null ? a : b`), which sfge already handles.
Failing that, an explicit unsupported-construct violation would at least make the lost coverage visible. Shape D in particular should not be able to take down an entire scan.
### Operating System
macOS 26.5.2
### Salesforce CLI Version
@salesforce/cli/2.147.7 darwin-arm64 node-v24.5.0
### Code Analyzer Plugin (code-analyzer) Version
code-analyzer 5.15.0
### Node Version
v24.5.0
### Java Version
openjdk version "11.0.32" 2026-07-21
### Python Version
N/A
### Additional Context (Screenshots, Files, etc)
`??` is idiomatic in any codebase on API 60 or later.
Shapes A-C each silently abandon one `@AuraEnabled` entry point — it produces no FLS findings, and nothing in the summary says coverage was lost. That is quiet in exactly the direction that matters for a security rule. In our codebase these three account for three distinct crash signatures.
Shape D is worse: one `??` in one for-loop header takes a team from a working scan to no security analysis at all. We happen not to have that shape, which is the only reason our runs produce results.
Related history: #1380, #1389 and #1608 covered `??` failing to *parse*. Parsing was fixed; graph modelling was not. #1240 is the same `validateParameterSize` throw as shape B but from a different trigger (`Cache.Partition.put`'s 3-argument overload), closed in the 2026-06-30 pre-v5 sweep.
### Workaround
Rewrite as an explicit ternary or null check:
```apex
String v = (s != null) ? s : '';
for (Account a : (aList != null ? aList : new List())) { ... }
```
That is a mechanical change but an unpleasant one to apply across a large codebase, and it means giving up a language feature to keep the security scanner working.
### Urgency
High
Contributor guide
Research direction
Start by reproducing the four cases with the supplied SFDX classes and sfge command. Trace the graph construction and child handling through BooleanExpressionVertex.java, ApexValue.java, ApexMapValue.java, MapEntryNodeVertex.java, and MethodPathBuilderVisitor.visitForEachStatement. Done means both operands are available to each consumer, the cases behave like the supported ternary form, and a malformed case cannot abort the entire scan.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- devtools, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100