[BUG][code-analyzer] sfge: the ?? null-coalescing operator is unmodelled — four distinct crashes, one of which aborts the entire scan
还没有人认领这个 Issue。
评估
调研方向
首先,使用提供的 SFDX 类和 sfge 命令复现这四种情况。通过 BooleanExpressionVertex.java、ApexValue.java、ApexMapValue.java、MapEntryNodeVertex.java 和 MethodPathBuilderVisitor.visitForEachStatement 跟踪图的构建和子节点处理。完成的标准是:每个消费者都能获得两个操作数,这些情况的行为与受支持的三元形式一致,并且格式错误的情况不能中止整个扫描。
由索引模型根据 Issue 内容生成。
描述
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
- 主要语言
- TypeScript
- 星标
- 241
- 派生
- 51
- PR 合并指标
- 30 天内没有已合并 PR
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 没有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
forcedotcom/code-analyzer 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 78/100
forcedotcom/code-analyzer#2094 ·
-
难度 1/5 1 小时以内 新手友好度 88/100
forcedotcom/code-analyzer#2093 ·
-
难度 2/5 1-3 小时 新手友好度 82/100
forcedotcom/code-analyzer#2091 ·
-
难度 2/5 1-3 小时 新手友好度 76/100
forcedotcom/code-analyzer#2090 ·
-
难度 3/5 1-2 天 新手友好度 72/100
forcedotcom/code-analyzer#2104 ·
查看 forcedotcom/code-analyzer 的全部 Issue
相似的 Issue
-
Unsampled pageload span is profiled when profileLifecycle is 'trace' (browser UI profiling)可能已有人在做 @Jxxunnn 今天认领。 未关闭Browser Waiting for: Product Owner
难度 2/5 1-3 小时 新手友好度 82/100
getsentry/sentry-javascript#25497 · 1 条评论 ·
维护者通常 1 天内回复
-
e2e-failure ready-to-code
难度 2/5 1-3 小时 新手友好度 77/100
redhat-developer/rhdh-plugin-export-overlays#4438 · 1 条评论 ·
维护者通常 1 天内回复
-
resource: destroying a resource calls AbortController.abort() without a reason, creating an AbortError with a stack trace each time可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭area: core
难度 2/5 1-3 小时 新手友好度 75/100
angular/angular#71422 · 1 条评论 ·
维护者通常 1 天内回复
-
bug status: needs triage
难度 2/5 1-3 小时 新手友好度 72/100
facebook/docusaurus#12739 · 1 条评论 ·
维护者通常 1 天内回复
-
factory-active task-identify-harness-labels-done task-identify-issue-type-done
难度 1/5 1-3 小时 新手友好度 68/100
维护者通常 1 天内回复