[BUG][code-analyzer] sfge: the ?? null-coalescing operator is unmodelled — four distinct crashes, one of which aborts the entire scan
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 52/100
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.
Description
Have you tried to resolve this issue yourself first?
- 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<X>())). 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.
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.
Map<String, String> record = new Map<String, String>();
record.put('subField', maybeNull ?? '');
C. ?? as a map-literal value — IndexOutOfBoundsException at MapEntryNodeVertex.java:38 (getChildren().get(1) on an entry with one child).
Map<String, Object> row = new Map<String, Object>{ '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.
for (Account a : aList ?? new List<Account>()) { 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
# 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.<init>(BooleanExpressionVertex.java:25)
at com.salesforce.graph.vertex.BooleanExpressionVertex.<init>(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.<init>(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
-
Create an empty SFDX project (
sfdx-project.jsonwith a singleforce-apppackage directory). -
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.// 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'); } } }// B public with sharing class CoalesceInPutArg { @AuraEnabled public static void run(String maybeNull) { Map<String, String> record = new Map<String, String>(); record.put('subField', maybeNull ?? ''); insert new Account(Name = record.get('subField')); } }// C public with sharing class CoalesceInMapLiteral { @AuraEnabled public static void run(String status) { Map<String, Object> row = new Map<String, Object>{ 'id' => '001', 'status' => status ?? 'N/A' }; insert new Account(Name = String.valueOf(row.get('id'))); } }// D — run this one in its own workspace public with sharing class CoalesceInForLoop { @AuraEnabled public static void run(List<Account> aList) { for (Account a : aList ?? new List<Account>()) { a.Name = 'x'; } update aList; } } -
Add
code-analyzer.yml:engines: sfge: java_thread_timeout: 900000 java_thread_count: 4 -
Run
sf code-analyzer run --rule-selector sfge --workspace . --config-file code-analyzer.yml. -
A, B and C each report an
InternalExecutionErrorand yield no findings for that entry point. D returns a single Sev-1UnexpectedEngineErrorand 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:
String v = (s != null) ? s : '';
for (Account a : (aList != null ? aList : new List<Account>())) { ... }
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
- Dominant language
- TypeScript
- Stars
- 240
- Forks
- 52
- PR merge metrics
- No merged PRs in 30d
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from forcedotcom/code-analyzer
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
forcedotcom/code-analyzer#2094 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
forcedotcom/code-analyzer#2093 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
forcedotcom/code-analyzer#2091 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
forcedotcom/code-analyzer#2090 ·
-
Difficulty 3/5 1-2 days Newbie friendliness 68/100
forcedotcom/code-analyzer#2101 ·
All issues in forcedotcom/code-analyzer
Similar issues
-
bug HemiStake
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
hemilabs/ui-monorepo#2413 ·
Maintainers usually reply within 1 day
-
component/ui framework/react kind/bug language/javascript
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
meshery/meshery#22216 · 3 comments ·
Maintainers usually reply within 1 day
-
type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
paperclipai/paperclip#14982 ·
Maintainers usually reply within 1 day
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 95/100
lingdojo/kana-dojo#31515 · 1 comment · 5 reactions ·
Maintainers usually reply within 1 day