al-sem 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +361 -0
- package/package.json +64 -0
- package/scripts/d40-diff.ts +44 -0
- package/scripts/fetch-native-parser.ts +179 -0
- package/scripts/precision-sample.ts +99 -0
- package/scripts/precision-study.ts +42 -0
- package/scripts/precision-tabulate.ts +52 -0
- package/src/cli/baseline.ts +31 -0
- package/src/cli/diff.ts +199 -0
- package/src/cli/events-chains.ts +56 -0
- package/src/cli/events-fanout.ts +87 -0
- package/src/cli/exit-code.ts +30 -0
- package/src/cli/fingerprint-indexes.ts +130 -0
- package/src/cli/fingerprint-query.ts +543 -0
- package/src/cli/fingerprint-witness.ts +493 -0
- package/src/cli/fingerprint.ts +292 -0
- package/src/cli/format-compact-json.ts +45 -0
- package/src/cli/format-events.ts +77 -0
- package/src/cli/format-fingerprint.ts +295 -0
- package/src/cli/format-html.ts +503 -0
- package/src/cli/format-json.ts +13 -0
- package/src/cli/format-policy.ts +95 -0
- package/src/cli/format-sarif.ts +186 -0
- package/src/cli/format-terminal.ts +153 -0
- package/src/cli/index.ts +566 -0
- package/src/cli/policy.ts +204 -0
- package/src/config/roots-config.ts +302 -0
- package/src/deps/cache-versions.ts +74 -0
- package/src/deps/canonical-json.ts +27 -0
- package/src/deps/dependency-artifact.ts +144 -0
- package/src/deps/dependency-cache.ts +262 -0
- package/src/deps/dependency-dag.ts +128 -0
- package/src/deps/dependency-package-discovery.ts +85 -0
- package/src/deps/dependency-pipeline.ts +483 -0
- package/src/deps/dependency-projection.ts +211 -0
- package/src/deps/dependency-resolver.ts +154 -0
- package/src/deps/workspace-dependencies.ts +114 -0
- package/src/detectors/capability-query.ts +145 -0
- package/src/detectors/confidence.ts +52 -0
- package/src/detectors/d1-db-op-in-loop.ts +457 -0
- package/src/detectors/d10-self-modifying-loop.ts +114 -0
- package/src/detectors/d11-modify-without-get.ts +129 -0
- package/src/detectors/d12-dead-integration-event.ts +81 -0
- package/src/detectors/d13-cross-app-internal-call.ts +105 -0
- package/src/detectors/d14-dead-routine.ts +151 -0
- package/src/detectors/d16-obsolete-routine-call.ts +94 -0
- package/src/detectors/d17-min-version-drift.ts +157 -0
- package/src/detectors/d18-constant-filter-in-loop.ts +151 -0
- package/src/detectors/d19-unused-parameter.ts +116 -0
- package/src/detectors/d2-event-fanout-in-loop.ts +240 -0
- package/src/detectors/d20-unreachable-after-exit.ts +92 -0
- package/src/detectors/d21-read-without-load.ts +128 -0
- package/src/detectors/d22-flowfield-without-calcfields.ts +168 -0
- package/src/detectors/d29-subscriber-modify-on-event-record.ts +163 -0
- package/src/detectors/d3-load-state.ts +72 -0
- package/src/detectors/d3-missing-setloadfields.ts +234 -0
- package/src/detectors/d32-constant-boolean-parameter.ts +185 -0
- package/src/detectors/d33-unfiltered-bulk-write.ts +173 -0
- package/src/detectors/d34-commit-in-loop.ts +206 -0
- package/src/detectors/d35-commit-in-event-subscriber.ts +138 -0
- package/src/detectors/d36-late-setloadfields.ts +162 -0
- package/src/detectors/d37-validate-without-persist.ts +271 -0
- package/src/detectors/d38-subscriber-to-obsolete-event.ts +140 -0
- package/src/detectors/d39-record-left-dirty-across-chain.ts +165 -0
- package/src/detectors/d4-repeated-lookup-in-loop.ts +128 -0
- package/src/detectors/d40-transitive-load-missing.ts +217 -0
- package/src/detectors/d41-transitive-filter-loss.ts +200 -0
- package/src/detectors/d42-cross-call-wrong-setloadfields.ts +243 -0
- package/src/detectors/d43-event-ishandled-skip.ts +257 -0
- package/src/detectors/d44-event-multi-subscriber-overlap.ts +223 -0
- package/src/detectors/d45-event-transitive-table-exposure.ts +159 -0
- package/src/detectors/d5-set-based-opportunity.ts +162 -0
- package/src/detectors/d7-recursive-event-expansion.ts +151 -0
- package/src/detectors/d8-commit-in-transaction.ts +132 -0
- package/src/detectors/d9-transaction-span-summary.ts +107 -0
- package/src/detectors/detector-context.ts +121 -0
- package/src/detectors/finding-grouping.ts +61 -0
- package/src/detectors/path-merge.ts +174 -0
- package/src/detectors/registry.ts +176 -0
- package/src/detectors/table-display.ts +42 -0
- package/src/diff/diff-abi.ts +195 -0
- package/src/diff/diff-capabilities.ts +179 -0
- package/src/diff/diff-engine.ts +146 -0
- package/src/diff/diff-events.ts +323 -0
- package/src/diff/diff-identity.ts +73 -0
- package/src/diff/diff-indexes.ts +199 -0
- package/src/diff/diff-permissions.ts +260 -0
- package/src/diff/diff-policy.ts +101 -0
- package/src/diff/diff-preflight.ts +66 -0
- package/src/diff/diff-renames.ts +104 -0
- package/src/diff/diff-schema.ts +232 -0
- package/src/diff/format-diff.ts +148 -0
- package/src/engine/attribute-parser.ts +50 -0
- package/src/engine/capability-cone.ts +531 -0
- package/src/engine/combined-graph.ts +357 -0
- package/src/engine/control-flow-walker.ts +1317 -0
- package/src/engine/dispatch-sites.ts +199 -0
- package/src/engine/effect-lattice.ts +81 -0
- package/src/engine/entry-points.ts +57 -0
- package/src/engine/event-flow.ts +524 -0
- package/src/engine/event-relay.ts +92 -0
- package/src/engine/op-classification.ts +92 -0
- package/src/engine/path-walker.ts +189 -0
- package/src/engine/reverse-call-graph.ts +23 -0
- package/src/engine/root-classifier-overlay.ts +194 -0
- package/src/engine/root-classifier.ts +135 -0
- package/src/engine/scc.ts +110 -0
- package/src/engine/source-anchor.ts +25 -0
- package/src/engine/summary-context.ts +104 -0
- package/src/engine/summary-engine.ts +296 -0
- package/src/engine/summary-runner.ts +560 -0
- package/src/engine/transaction-spans.ts +112 -0
- package/src/engine/uncertainty-util.ts +54 -0
- package/src/hash.ts +31 -0
- package/src/index/attribute-from-node.ts +141 -0
- package/src/index/callee-from-node.ts +181 -0
- package/src/index/capability/background.ts +90 -0
- package/src/index/capability/commit.ts +44 -0
- package/src/index/capability/dispatch.ts +164 -0
- package/src/index/capability/events.ts +65 -0
- package/src/index/capability/extractor.ts +124 -0
- package/src/index/capability/file-blob.ts +137 -0
- package/src/index/capability/http.ts +159 -0
- package/src/index/capability/hyperlink.ts +60 -0
- package/src/index/capability/isolated-storage.ts +179 -0
- package/src/index/capability/table.ts +113 -0
- package/src/index/capability/telemetry.ts +84 -0
- package/src/index/capability/ui.ts +55 -0
- package/src/index/capability/value-source.ts +202 -0
- package/src/index/expression-from-node.ts +117 -0
- package/src/index/indexer.ts +102 -0
- package/src/index/intraprocedural-body.ts +1467 -0
- package/src/index/intraprocedural-ops.ts +253 -0
- package/src/index/intraprocedural-refs.ts +188 -0
- package/src/index/object-indexer.ts +279 -0
- package/src/index/routine-indexer.ts +282 -0
- package/src/index/routine-signature.ts +46 -0
- package/src/index/variable-indexer.ts +134 -0
- package/src/index/variable-initializer-extractor.ts +155 -0
- package/src/index/variable-type-normalizer.ts +83 -0
- package/src/index.ts +267 -0
- package/src/mcp/server.ts +72 -0
- package/src/mcp/session.ts +49 -0
- package/src/mcp/tools/explain-path.ts +75 -0
- package/src/mcp/tools/get-analysis-health.ts +62 -0
- package/src/mcp/tools/get-finding.ts +47 -0
- package/src/mcp/tools/get-routine-summary.ts +126 -0
- package/src/mcp/tools/list-findings.ts +85 -0
- package/src/mcp/tools/list-hotspots.ts +78 -0
- package/src/mcp/tools/list-rollups.ts +103 -0
- package/src/mcp/tools/validators.ts +25 -0
- package/src/model/attributes.ts +120 -0
- package/src/model/callee.ts +45 -0
- package/src/model/capability.ts +187 -0
- package/src/model/coverage.ts +85 -0
- package/src/model/entities.ts +628 -0
- package/src/model/expression.ts +98 -0
- package/src/model/finding.ts +110 -0
- package/src/model/graph-edge.ts +93 -0
- package/src/model/graph.ts +62 -0
- package/src/model/identity.ts +81 -0
- package/src/model/ids.ts +90 -0
- package/src/model/index.ts +13 -0
- package/src/model/model.ts +51 -0
- package/src/model/permission.ts +76 -0
- package/src/model/root-classification.ts +116 -0
- package/src/model/stable-identity.ts +102 -0
- package/src/model/summary.ts +96 -0
- package/src/parser/ast.ts +82 -0
- package/src/parser/native/ffi.ts +145 -0
- package/src/parser/native/parse-index-pool.ts +148 -0
- package/src/parser/native/parse-index-worker.ts +94 -0
- package/src/parser/native/wrapper.ts +353 -0
- package/src/parser/parser-init.ts +43 -0
- package/src/perf/profiler.ts +66 -0
- package/src/policy/policy-default.yaml +83 -0
- package/src/policy/policy-engine.ts +339 -0
- package/src/policy/policy-loader.ts +257 -0
- package/src/policy/policy-schema.json +379 -0
- package/src/policy/policy-types.ts +81 -0
- package/src/policy/predicate-compiler.ts +151 -0
- package/src/policy/predicate-evaluator.ts +267 -0
- package/src/policy/predicate-fields.ts +439 -0
- package/src/projection/actionable-anchor.ts +48 -0
- package/src/projection/finding-filters.ts +44 -0
- package/src/projection/finding-fingerprint.ts +54 -0
- package/src/projection/finding-groups.ts +41 -0
- package/src/projection/finding-summary.ts +110 -0
- package/src/projection/rollup-findings.ts +105 -0
- package/src/providers/discover.ts +88 -0
- package/src/providers/external.ts +46 -0
- package/src/providers/types.ts +36 -0
- package/src/providers/workspace.ts +117 -0
- package/src/resolve/call-resolver.ts +117 -0
- package/src/resolve/coverage.ts +61 -0
- package/src/resolve/event-graph.ts +166 -0
- package/src/resolve/implicit-edges.ts +53 -0
- package/src/resolve/record-types.ts +36 -0
- package/src/resolve/resolver.ts +23 -0
- package/src/resolve/semantic-graph.ts +29 -0
- package/src/resolve/symbol-table.ts +69 -0
- package/src/snapshot/app-snapshot.ts +74 -0
- package/src/snapshot/compose.ts +100 -0
- package/src/snapshot/derive/callsite-evidence.ts +76 -0
- package/src/snapshot/derive/capability-facts.ts +70 -0
- package/src/snapshot/derive/contracts.ts +131 -0
- package/src/snapshot/derive/coverage.ts +35 -0
- package/src/snapshot/derive/event-declarations.ts +140 -0
- package/src/snapshot/derive/identity-table.ts +58 -0
- package/src/snapshot/derive/inputs.ts +91 -0
- package/src/snapshot/derive/operation-evidence.ts +70 -0
- package/src/snapshot/derive/permissions.ts +186 -0
- package/src/snapshot/derive/root-classifications.ts +56 -0
- package/src/snapshot/derive/schema.ts +130 -0
- package/src/snapshot/derive/typed-edges.ts +60 -0
- package/src/snapshot/derive/workspace-fingerprint.ts +19 -0
- package/src/snapshot/deserialize.ts +40 -0
- package/src/snapshot/serialize-cbor-gz.ts +12 -0
- package/src/snapshot/serialize-cbor.ts +19 -0
- package/src/snapshot/serialize-json.ts +22 -0
- package/src/snapshot/shard.ts +134 -0
- package/src/snapshot/types.ts +181 -0
- package/src/symbols/app-manifest.ts +96 -0
- package/src/symbols/app-package-zip.ts +50 -0
- package/src/symbols/embedded-source-reader.ts +41 -0
- package/src/symbols/package-hash.ts +81 -0
- package/src/symbols/symbol-reader.ts +101 -0
- package/src/symbols/symbol-reference-parser.ts +378 -0
- package/src/symbols/symbol-reference-reader.ts +27 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
2
|
+
import { beforeAnchor } from "../engine/source-anchor.ts";
|
|
3
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
4
|
+
import { roleOf } from "../model/entities.ts";
|
|
5
|
+
import type { CallSite, RecordOperation } from "../model/entities.ts";
|
|
6
|
+
import type { DetectorStats, EvidenceStep, Finding } from "../model/finding.ts";
|
|
7
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
8
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
9
|
+
import { toConfidence } from "./confidence.ts";
|
|
10
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
11
|
+
|
|
12
|
+
const PERSIST_OPS: ReadonlySet<string> = new Set(["Modify", "ModifyAll", "Insert"]);
|
|
13
|
+
const RESET_LIKE_OPS: ReadonlySet<string> = new Set([
|
|
14
|
+
"Init", // reinitialises the in-memory record (drops pending Validate changes)
|
|
15
|
+
"Reset",
|
|
16
|
+
"Get", // reloads — pending Validate state is overwritten
|
|
17
|
+
"FindFirst",
|
|
18
|
+
"FindLast",
|
|
19
|
+
"FindSet",
|
|
20
|
+
"Find",
|
|
21
|
+
"Next",
|
|
22
|
+
"Copy", // overwrites with another record's state
|
|
23
|
+
"TransferFields",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* D37 — `Validate(...)` on a record variable with no subsequent `Modify` / `ModifyAll`
|
|
28
|
+
* / `Insert` to persist the change before the record's state is overwritten or the
|
|
29
|
+
* routine exits. `Validate` runs field-validation logic and changes the IN-MEMORY
|
|
30
|
+
* record; without a later persist call, the field write is silently discarded.
|
|
31
|
+
*
|
|
32
|
+
* Detection (intra-routine, source-ordered):
|
|
33
|
+
* - for each Validate op V on record-var R;
|
|
34
|
+
* - walk subsequent ops on R in source order;
|
|
35
|
+
* - if a Modify/ModifyAll/Insert appears before any state-overwriting op
|
|
36
|
+
* (Init/Reset/Get/Find/FindFirst/FindLast/FindSet/Next/Copy/TransferFields),
|
|
37
|
+
* the Validate is persisted — skip;
|
|
38
|
+
* - otherwise the Validate is unpersisted — flag.
|
|
39
|
+
*
|
|
40
|
+
* Suppressions:
|
|
41
|
+
* - temporary records (no DB persistence concept) — skippedTempRecord
|
|
42
|
+
* - by-var parameter records (caller may persist after returning) — skippedParameter
|
|
43
|
+
* - calls passing R to a helper whose summary may persist — skippedHelperMayPersist
|
|
44
|
+
* Note: persistsCurrentRecord = "yes" is a MAY fact (helper persists on at least
|
|
45
|
+
* one path). True must-persist tracking is a documented carry-forward — see
|
|
46
|
+
* docs/superpowers/specs/2026-05-17-al-sem-record-flow-framework-design.md.
|
|
47
|
+
* - calls passing R to an opaque or unresolved helper — skippedHelperPersistsUnknown
|
|
48
|
+
*
|
|
49
|
+
* The forwardedAfter regex from the original implementation is replaced with
|
|
50
|
+
* structural CallSite.argumentBindings (Phase 1 of the record-flow framework).
|
|
51
|
+
*
|
|
52
|
+
* Severity: `medium`. Real bug but the conservative suppressions keep it actionable.
|
|
53
|
+
* Confidence: `possible` (we don't have interprocedural persist tracking).
|
|
54
|
+
*
|
|
55
|
+
* Known precision gaps:
|
|
56
|
+
* - The `helperMayPersist` suppression is conservative — persistsCurrentRecord = "yes"
|
|
57
|
+
* is a MAY fact; if the helper only persists on some paths, a Validate in the caller
|
|
58
|
+
* that's followed by a non-persisting path through the helper is still flagged
|
|
59
|
+
* incorrectly. Must-persist tracking is the documented carry-forward for this.
|
|
60
|
+
* - We don't track `TransferFields` direction (it's a write to the target record).
|
|
61
|
+
* We treat it as state-overwriting on the receiver, which suppresses correctly when
|
|
62
|
+
* R is the SOURCE; if R is the TARGET, we conservatively still treat it as overwriting
|
|
63
|
+
* its prior in-memory state, which is the safe call.
|
|
64
|
+
*/
|
|
65
|
+
export function detectD37(
|
|
66
|
+
model: SemanticModel,
|
|
67
|
+
graph: CombinedGraph,
|
|
68
|
+
ctx: DetectorContext,
|
|
69
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
70
|
+
const findings: Finding[] = [];
|
|
71
|
+
let candidatesConsidered = 0;
|
|
72
|
+
let skippedPersisted = 0;
|
|
73
|
+
let skippedHelperMayPersist = 0;
|
|
74
|
+
let skippedHelperPersistsUnknown = 0;
|
|
75
|
+
let skippedTempRecord = 0;
|
|
76
|
+
let skippedParameter = 0;
|
|
77
|
+
|
|
78
|
+
for (const routine of model.routines) {
|
|
79
|
+
if (roleOf(routine) !== "primary") continue;
|
|
80
|
+
if (!routine.bodyAvailable) continue;
|
|
81
|
+
if (routine.parseIncomplete) continue;
|
|
82
|
+
|
|
83
|
+
const paramRecordNames = new Set(
|
|
84
|
+
routine.features.recordVariables
|
|
85
|
+
.filter((rv) => rv.isParameter)
|
|
86
|
+
.map((rv) => rv.name.toLowerCase()),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
for (const op of routine.features.recordOperations) {
|
|
90
|
+
if (op.op !== "Validate") continue;
|
|
91
|
+
candidatesConsidered++;
|
|
92
|
+
const varKey = op.recordVariableName.toLowerCase();
|
|
93
|
+
if (op.tempState.kind === "known" && op.tempState.value === true) {
|
|
94
|
+
skippedTempRecord++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (paramRecordNames.has(varKey)) {
|
|
98
|
+
skippedParameter++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Walk subsequent ops in source order, looking for persistence vs reset.
|
|
103
|
+
const verdict = laterPersisted(routine.features.recordOperations, varKey, op);
|
|
104
|
+
if (verdict === "persisted") {
|
|
105
|
+
skippedPersisted++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Phase 3: precise verdict using callee.persistsCurrentRecord summary.
|
|
110
|
+
const sourceVariableNameLc = op.recordVariableName.toLowerCase();
|
|
111
|
+
const sourceRecordVariableId = op.recordVariableId;
|
|
112
|
+
const helperVerdict = postValidateHelperVerdict(
|
|
113
|
+
routine,
|
|
114
|
+
sourceRecordVariableId,
|
|
115
|
+
sourceVariableNameLc,
|
|
116
|
+
op,
|
|
117
|
+
graph,
|
|
118
|
+
ctx,
|
|
119
|
+
);
|
|
120
|
+
if (helperVerdict === "suppress-may-persist") {
|
|
121
|
+
skippedHelperMayPersist++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (helperVerdict === "suppress-unknown") {
|
|
125
|
+
skippedHelperPersistsUnknown++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// "do-not-suppress" — helper provably doesn't persist; fall through to emit.
|
|
129
|
+
|
|
130
|
+
emit(routine, op, findings, model);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const sorted = findings.sort((a, b) => compareStrings(a.id, b.id));
|
|
135
|
+
return {
|
|
136
|
+
findings: sorted,
|
|
137
|
+
stats: {
|
|
138
|
+
detector: "d37-validate-without-persist",
|
|
139
|
+
candidatesConsidered,
|
|
140
|
+
findingsEmitted: sorted.length,
|
|
141
|
+
skipped: {
|
|
142
|
+
...(skippedPersisted > 0 ? { persisted: skippedPersisted } : {}),
|
|
143
|
+
...(skippedHelperMayPersist > 0 ? { helperMayPersist: skippedHelperMayPersist } : {}),
|
|
144
|
+
...(skippedHelperPersistsUnknown > 0
|
|
145
|
+
? { helperPersistsUnknown: skippedHelperPersistsUnknown }
|
|
146
|
+
: {}),
|
|
147
|
+
...(skippedTempRecord > 0 ? { tempRecord: skippedTempRecord } : {}),
|
|
148
|
+
...(skippedParameter > 0 ? { parameter: skippedParameter } : {}),
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function laterPersisted(
|
|
155
|
+
ops: RecordOperation[],
|
|
156
|
+
varKey: string,
|
|
157
|
+
validateOp: RecordOperation,
|
|
158
|
+
): "persisted" | "unpersisted" {
|
|
159
|
+
// Iterate ops in source order; the first persist OR reset wins.
|
|
160
|
+
const sorted = ops
|
|
161
|
+
.filter((o) => o.recordVariableName.toLowerCase() === varKey)
|
|
162
|
+
.filter((o) => beforeAnchor(validateOp.sourceAnchor, o.sourceAnchor))
|
|
163
|
+
.sort((a, b) => (beforeAnchor(a.sourceAnchor, b.sourceAnchor) ? -1 : 1));
|
|
164
|
+
for (const o of sorted) {
|
|
165
|
+
if (PERSIST_OPS.has(o.op)) return "persisted";
|
|
166
|
+
if (RESET_LIKE_OPS.has(o.op)) return "unpersisted";
|
|
167
|
+
}
|
|
168
|
+
return "unpersisted";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* After the Validate op, walk callsites in source order. For each callsite that
|
|
173
|
+
* forwards the same record variable to a callee, look at the callee's summary:
|
|
174
|
+
* - persistsCurrentRecord === "yes" → may persist (may-fact); suppress with
|
|
175
|
+
* helperMayPersist counter. Must-persist tracking is a documented
|
|
176
|
+
* carry-forward — would close this conservative suppression.
|
|
177
|
+
* - persistsCurrentRecord === "no" → helper provably doesn't persist; do not
|
|
178
|
+
* suppress (the Validate's unpersistence is the real bug).
|
|
179
|
+
* - persistsCurrentRecord === "unknown" → conservative suppress with
|
|
180
|
+
* helperPersistsUnknown counter.
|
|
181
|
+
*
|
|
182
|
+
* Returns the verdict for the entire post-Validate sequence: if any post-Validate
|
|
183
|
+
* helper might persist (yes or unknown), the Validate is suppressed; only when
|
|
184
|
+
* EVERY forwarding helper provably doesn't persist do we fall through to emit.
|
|
185
|
+
*/
|
|
186
|
+
function postValidateHelperVerdict(
|
|
187
|
+
routine: { id: string; features: { callSites: CallSite[] } },
|
|
188
|
+
sourceRecordVariableId: string | undefined,
|
|
189
|
+
sourceVariableNameLc: string,
|
|
190
|
+
validateOp: { sourceAnchor: { range: { startLine: number; startColumn: number } } },
|
|
191
|
+
graph: CombinedGraph,
|
|
192
|
+
ctx: DetectorContext,
|
|
193
|
+
): "suppress-may-persist" | "suppress-unknown" | "do-not-suppress" {
|
|
194
|
+
for (const cs of routine.features.callSites) {
|
|
195
|
+
if (!beforeAnchor(validateOp.sourceAnchor, cs.sourceAnchor)) continue;
|
|
196
|
+
for (const binding of cs.argumentBindings) {
|
|
197
|
+
// Literals / non-record expressions can't forward a record — skip silently.
|
|
198
|
+
if (binding.bindingResolution === "non-record-arg") continue;
|
|
199
|
+
// Match by stable id when both sides have one; fall back to name (lowercased).
|
|
200
|
+
const matchesById =
|
|
201
|
+
binding.sourceRecordVariableId !== undefined &&
|
|
202
|
+
sourceRecordVariableId !== undefined &&
|
|
203
|
+
binding.sourceRecordVariableId === sourceRecordVariableId;
|
|
204
|
+
const matchesByName = binding.sourceVariableName === sourceVariableNameLc;
|
|
205
|
+
if (!matchesById && !matchesByName) continue;
|
|
206
|
+
// Unresolved-callee / ambiguous: we cannot trust calleeParameterIsVar (it's left
|
|
207
|
+
// false as a placeholder by the indexer) nor look up a callee summary. The old
|
|
208
|
+
// regex-based forwardedAfter suppressed every callsite regardless of resolution;
|
|
209
|
+
// preserve that conservatism for unresolved bindings that DO target our record.
|
|
210
|
+
if (binding.bindingResolution !== "resolved") return "suppress-unknown";
|
|
211
|
+
// By-value callees can't persist the caller's record (the in-memory copy stays in callee).
|
|
212
|
+
if (!binding.calleeParameterIsVar) continue;
|
|
213
|
+
const edge = (graph.edgesByFrom.get(routine.id) ?? []).find((e) => e.callsiteId === cs.id);
|
|
214
|
+
if (edge?.to === undefined) return "suppress-unknown";
|
|
215
|
+
const callee = ctx.routineById.get(edge.to);
|
|
216
|
+
const calleeRole = callee?.summary?.parameterRoles.find(
|
|
217
|
+
(r) => r.parameterIndex === binding.parameterIndex,
|
|
218
|
+
);
|
|
219
|
+
if (calleeRole === undefined || callee?.bodyAvailable === false) {
|
|
220
|
+
return "suppress-unknown";
|
|
221
|
+
}
|
|
222
|
+
switch (calleeRole.persistsCurrentRecord) {
|
|
223
|
+
case "yes":
|
|
224
|
+
return "suppress-may-persist";
|
|
225
|
+
case "unknown":
|
|
226
|
+
return "suppress-unknown";
|
|
227
|
+
case "no" /* helper provably doesn't persist — keep walking */:
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return "do-not-suppress";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function emit(
|
|
236
|
+
routine: { id: string; objectId: string; name: string },
|
|
237
|
+
op: RecordOperation,
|
|
238
|
+
findings: Finding[],
|
|
239
|
+
model: SemanticModel,
|
|
240
|
+
): void {
|
|
241
|
+
const path: EvidenceStep[] = [
|
|
242
|
+
{
|
|
243
|
+
routineId: routine.id,
|
|
244
|
+
operationId: op.id,
|
|
245
|
+
sourceAnchor: op.sourceAnchor,
|
|
246
|
+
note: `Validate on ${op.recordVariableName} with no later Modify/Insert before the record is reloaded or the routine exits`,
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
const finding: Finding = {
|
|
250
|
+
id: `d37/${routine.id}/${op.id}`,
|
|
251
|
+
rootCauseKey: `d37/${routine.id}/${op.id}`,
|
|
252
|
+
detector: "d37-validate-without-persist",
|
|
253
|
+
title: "Validate changes are not persisted",
|
|
254
|
+
rootCause: `${routine.name} calls Validate on ${op.recordVariableName} but never persists the change with Modify / ModifyAll / Insert before the record is reloaded or the routine returns — the field write is discarded.`,
|
|
255
|
+
severity: "medium",
|
|
256
|
+
confidence: toConfidence([], "possible"),
|
|
257
|
+
primaryLocation: op.sourceAnchor,
|
|
258
|
+
evidencePath: path,
|
|
259
|
+
affectedObjects: [routine.objectId],
|
|
260
|
+
affectedTables: op.tableId !== undefined ? [op.tableId] : [],
|
|
261
|
+
fixOptions: [
|
|
262
|
+
{
|
|
263
|
+
description: `Add ${op.recordVariableName}.Modify() after the Validate (or ${op.recordVariableName}.Insert() if the record is new). If the Validate is intentional (only running validation logic, not persisting), document the intent.`,
|
|
264
|
+
safety: "high",
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
provenance: [{ source: "tree-sitter" }],
|
|
268
|
+
};
|
|
269
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
270
|
+
findings.push(finding);
|
|
271
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { parseRoutineAttributes } from "../engine/attribute-parser.ts";
|
|
2
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
3
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
4
|
+
import { roleOf } from "../model/entities.ts";
|
|
5
|
+
import type { DetectorStats, EvidenceStep, Finding } from "../model/finding.ts";
|
|
6
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
7
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
8
|
+
import { toConfidence } from "./confidence.ts";
|
|
9
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* D38 — primary-app `[EventSubscriber(...)]` bound to a publisher whose publisher
|
|
13
|
+
* routine carries `[Obsolete(...)]`.
|
|
14
|
+
*
|
|
15
|
+
* Why al-sem: needs the **event graph** (publisher routine ↔ subscriber routine edges
|
|
16
|
+
* resolved across apps, including into dep `.app` packages) and per-routine attribute
|
|
17
|
+
* parsing. A per-file scanner can read the subscriber's attribute string but cannot
|
|
18
|
+
* resolve the publisher's routine — that requires the cross-app event resolver.
|
|
19
|
+
*
|
|
20
|
+
* Closest neighbour: D16 (call to obsolete routine) — different surface. D16 flags a
|
|
21
|
+
* *call* edge; D38 flags an *event subscription* edge. The subscriber is never called
|
|
22
|
+
* by primary code (the publisher dispatches it), so D16 misses it.
|
|
23
|
+
*
|
|
24
|
+
* Severity by publisher's obsolete state:
|
|
25
|
+
* - Pending → info (deprecated, plan migration; subscriber still fires today)
|
|
26
|
+
* - Removed → high (publisher is gone or about to be — subscriber will never fire)
|
|
27
|
+
*
|
|
28
|
+
* Skipped:
|
|
29
|
+
* - non-resolved edges (resolution !== "resolved"): can't trust the publisher
|
|
30
|
+
* identity, so can't reason about its attributes.
|
|
31
|
+
* - publisher symbol with no publisherRoutineId: trigger-style or synthetic events
|
|
32
|
+
* don't have a routine carrying [Obsolete].
|
|
33
|
+
* - subscriber not in primary app: user can't edit dep subscribers.
|
|
34
|
+
*/
|
|
35
|
+
export function detectD38(
|
|
36
|
+
model: SemanticModel,
|
|
37
|
+
_graph: CombinedGraph,
|
|
38
|
+
ctx: DetectorContext,
|
|
39
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
40
|
+
const findings: Finding[] = [];
|
|
41
|
+
const { routineById } = ctx;
|
|
42
|
+
|
|
43
|
+
const eventById = new Map(model.eventGraph.events.map((e) => [e.id, e]));
|
|
44
|
+
|
|
45
|
+
let candidatesConsidered = 0;
|
|
46
|
+
let skippedUnresolved = 0;
|
|
47
|
+
let skippedNoPublisherRoutine = 0;
|
|
48
|
+
let skippedPublisherMissing = 0;
|
|
49
|
+
let skippedPublisherNotObsolete = 0;
|
|
50
|
+
|
|
51
|
+
for (const edge of model.eventGraph.edges) {
|
|
52
|
+
if (edge.resolution !== "resolved") {
|
|
53
|
+
skippedUnresolved++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const subscriber = routineById.get(edge.subscriberRoutineId);
|
|
57
|
+
if (subscriber === undefined) continue;
|
|
58
|
+
if (roleOf(subscriber) !== "primary") continue;
|
|
59
|
+
if (subscriber.parseIncomplete) continue;
|
|
60
|
+
|
|
61
|
+
const event = eventById.get(edge.eventId);
|
|
62
|
+
if (event === undefined) continue;
|
|
63
|
+
if (event.publisherRoutineId === undefined) {
|
|
64
|
+
skippedNoPublisherRoutine++;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const publisher = routineById.get(event.publisherRoutineId);
|
|
68
|
+
if (publisher === undefined) {
|
|
69
|
+
skippedPublisherMissing++;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
candidatesConsidered++;
|
|
73
|
+
|
|
74
|
+
const attrs = parseRoutineAttributes(publisher);
|
|
75
|
+
if (attrs.obsoleteState === undefined) {
|
|
76
|
+
skippedPublisherNotObsolete++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const sev: Finding["severity"] = attrs.obsoleteState === "Removed" ? "high" : "info";
|
|
81
|
+
const path: EvidenceStep[] = [
|
|
82
|
+
{
|
|
83
|
+
routineId: subscriber.id,
|
|
84
|
+
sourceAnchor: subscriber.sourceAnchor,
|
|
85
|
+
note: `[EventSubscriber] subscribes to '${event.eventName}'`,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
routineId: publisher.id,
|
|
89
|
+
sourceAnchor: publisher.sourceAnchor,
|
|
90
|
+
note: `publisher ${publisher.name} is [Obsolete(${attrs.obsoleteState})]${attrs.obsoleteReason ? ` — ${attrs.obsoleteReason}` : ""}`,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const finding: Finding = {
|
|
95
|
+
id: `d38/${subscriber.id}/${event.id}`,
|
|
96
|
+
rootCauseKey: `d38/${subscriber.id}/${event.id}`,
|
|
97
|
+
detector: "d38-subscriber-to-obsolete-event",
|
|
98
|
+
title: `Subscriber bound to obsolete event (${attrs.obsoleteState})`,
|
|
99
|
+
rootCause:
|
|
100
|
+
attrs.obsoleteState === "Removed"
|
|
101
|
+
? `${subscriber.name} subscribes to '${event.eventName}', whose publisher ${publisher.name} is [Obsolete(Removed)] — the subscriber will stop firing once the publisher is removed.`
|
|
102
|
+
: `${subscriber.name} subscribes to '${event.eventName}', whose publisher ${publisher.name} is [Obsolete(Pending)] — plan a migration to the successor before the publisher is removed.`,
|
|
103
|
+
severity: sev,
|
|
104
|
+
confidence: toConfidence([], "confirmed"),
|
|
105
|
+
primaryLocation: subscriber.sourceAnchor,
|
|
106
|
+
evidencePath: path,
|
|
107
|
+
affectedObjects: [subscriber.objectId, publisher.objectId].sort(),
|
|
108
|
+
affectedTables: [],
|
|
109
|
+
fixOptions: [
|
|
110
|
+
{
|
|
111
|
+
description:
|
|
112
|
+
attrs.obsoleteReason ??
|
|
113
|
+
"Migrate the subscriber to the documented successor event; if none exists, remove the subscription once the publisher is gone.",
|
|
114
|
+
safety: "high",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
provenance: [{ source: "tree-sitter" }],
|
|
118
|
+
};
|
|
119
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
120
|
+
findings.push(finding);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const sorted = findings.sort((a, b) => compareStrings(a.id, b.id));
|
|
124
|
+
return {
|
|
125
|
+
findings: sorted,
|
|
126
|
+
stats: {
|
|
127
|
+
detector: "d38-subscriber-to-obsolete-event",
|
|
128
|
+
candidatesConsidered,
|
|
129
|
+
findingsEmitted: sorted.length,
|
|
130
|
+
skipped: {
|
|
131
|
+
...(skippedUnresolved > 0 ? { unresolved: skippedUnresolved } : {}),
|
|
132
|
+
...(skippedNoPublisherRoutine > 0 ? { noPublisherRoutine: skippedNoPublisherRoutine } : {}),
|
|
133
|
+
...(skippedPublisherMissing > 0 ? { publisherMissing: skippedPublisherMissing } : {}),
|
|
134
|
+
...(skippedPublisherNotObsolete > 0
|
|
135
|
+
? { publisherNotObsolete: skippedPublisherNotObsolete }
|
|
136
|
+
: {}),
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
2
|
+
import { beforeAnchor } from "../engine/source-anchor.ts";
|
|
3
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
4
|
+
import { roleOf } from "../model/entities.ts";
|
|
5
|
+
import type { DetectorStats, EvidenceStep, Finding } from "../model/finding.ts";
|
|
6
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
7
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
8
|
+
import { toConfidence } from "./confidence.ts";
|
|
9
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
10
|
+
|
|
11
|
+
const PERSIST_OPS: ReadonlySet<string> = new Set(["Modify", "Insert", "Rename"]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* D39 — record left dirty across helper chain.
|
|
15
|
+
*
|
|
16
|
+
* For each var-param P of every primary callee where the path-aware walker PROVED
|
|
17
|
+
* `dirtyAtExit[P] === "yes"` (at least one exit path leaves the record dirty after a
|
|
18
|
+
* Validate without a subsequent persist), walk the reverse call graph. Every primary
|
|
19
|
+
* caller that:
|
|
20
|
+
* (a) forwards a record to P (binding resolves to a local/parameter/implicit-rec source),
|
|
21
|
+
* (b) passes by-var (calleeParameterIsVar === true),
|
|
22
|
+
* (c) does NOT persist the same source variable after the callsite, and
|
|
23
|
+
* (d) does NOT pass it from a by-value parameter (parameter source that is not var)
|
|
24
|
+
* …is flagged: the Validate's field write is silently discarded across the chain.
|
|
25
|
+
*
|
|
26
|
+
* `dirtyAtExit === "unknown"` cases are counted in `unknownDirtyCallee` and NOT emitted;
|
|
27
|
+
* this protects D39 from the alert-fatigue regression D40 hit when the walker bailed to
|
|
28
|
+
* unknown on complex control flow.
|
|
29
|
+
*
|
|
30
|
+
* Skipped:
|
|
31
|
+
* - `callerPersists` — caller persists the source variable after the callsite
|
|
32
|
+
* - `unknownDirtyCallee` — callee's dirtyAtExit === "unknown" (counted but not emitted)
|
|
33
|
+
* - non-persisting source kinds (expression, global, unresolved — can't persist these)
|
|
34
|
+
* - by-value parameter source (callerSourceParameterIsVar === false) — bug is in the callee
|
|
35
|
+
*
|
|
36
|
+
* Severity: `medium`. Confidence: `likely`.
|
|
37
|
+
*/
|
|
38
|
+
export function detectD39(
|
|
39
|
+
model: SemanticModel,
|
|
40
|
+
_graph: CombinedGraph,
|
|
41
|
+
ctx: DetectorContext,
|
|
42
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
43
|
+
const findings: Finding[] = [];
|
|
44
|
+
const { routineById, reverseCallGraph } = ctx;
|
|
45
|
+
|
|
46
|
+
let candidatesConsidered = 0;
|
|
47
|
+
let skippedCallerPersists = 0;
|
|
48
|
+
let unknownDirtyCallee = 0;
|
|
49
|
+
|
|
50
|
+
for (const callee of model.routines) {
|
|
51
|
+
if (!callee.bodyAvailable) continue;
|
|
52
|
+
for (const role of callee.summary?.parameterRoles ?? []) {
|
|
53
|
+
if (role.dirtyAtExit === "unknown") {
|
|
54
|
+
unknownDirtyCallee++;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (role.dirtyAtExit !== "yes") continue;
|
|
58
|
+
|
|
59
|
+
// Find all resolved callers that forward a record to this var-parameter.
|
|
60
|
+
const callerEdges = reverseCallGraph.get(callee.id) ?? [];
|
|
61
|
+
for (const edge of callerEdges) {
|
|
62
|
+
if (edge.callsiteId === undefined) continue;
|
|
63
|
+
const caller = routineById.get(edge.from);
|
|
64
|
+
if (caller === undefined) continue;
|
|
65
|
+
if (roleOf(caller) !== "primary") continue;
|
|
66
|
+
if (!caller.bodyAvailable) continue;
|
|
67
|
+
|
|
68
|
+
const cs = caller.features.callSites.find((c) => c.id === edge.callsiteId);
|
|
69
|
+
if (cs === undefined) continue;
|
|
70
|
+
|
|
71
|
+
const binding = cs.argumentBindings.find(
|
|
72
|
+
(b) =>
|
|
73
|
+
b.parameterIndex === role.parameterIndex &&
|
|
74
|
+
b.bindingResolution === "resolved" &&
|
|
75
|
+
b.calleeParameterIsVar,
|
|
76
|
+
);
|
|
77
|
+
if (binding === undefined) continue;
|
|
78
|
+
|
|
79
|
+
// Only source kinds the caller can actually persist.
|
|
80
|
+
if (
|
|
81
|
+
binding.sourceKind !== "parameter" &&
|
|
82
|
+
binding.sourceKind !== "local" &&
|
|
83
|
+
binding.sourceKind !== "implicit-rec"
|
|
84
|
+
) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// For parameter sources, require the caller-side parameter to be var (otherwise
|
|
89
|
+
// the caller's copy is local — the bug is real but is D37's domain inside the callee).
|
|
90
|
+
if (binding.sourceKind === "parameter" && !binding.callerSourceParameterIsVar) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const sourceNameLc = binding.sourceVariableName;
|
|
95
|
+
if (sourceNameLc === undefined) continue;
|
|
96
|
+
|
|
97
|
+
candidatesConsidered++;
|
|
98
|
+
|
|
99
|
+
// Did caller persist the source variable after the callsite?
|
|
100
|
+
const persistedAfter = caller.features.recordOperations.some(
|
|
101
|
+
(op) =>
|
|
102
|
+
PERSIST_OPS.has(op.op) &&
|
|
103
|
+
op.recordVariableName.toLowerCase() === sourceNameLc &&
|
|
104
|
+
beforeAnchor(cs.sourceAnchor, op.sourceAnchor),
|
|
105
|
+
);
|
|
106
|
+
if (persistedAfter) {
|
|
107
|
+
skippedCallerPersists++;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Emit.
|
|
112
|
+
const path: EvidenceStep[] = [
|
|
113
|
+
{
|
|
114
|
+
routineId: caller.id,
|
|
115
|
+
callsiteId: cs.id,
|
|
116
|
+
sourceAnchor: binding.argumentAnchor,
|
|
117
|
+
note: `forwards ${binding.sourceVariableName} to ${callee.name}; never persists after the call`,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
routineId: callee.id,
|
|
121
|
+
sourceAnchor: callee.sourceAnchor,
|
|
122
|
+
note: `${callee.name} validates and exits dirty on at least one path`,
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const finding: Finding = {
|
|
127
|
+
id: `d39/${caller.id}/${cs.id}/${role.parameterIndex}`,
|
|
128
|
+
rootCauseKey: `d39/${caller.id}/${cs.id}/${role.parameterIndex}`,
|
|
129
|
+
detector: "d39-record-left-dirty-across-chain",
|
|
130
|
+
title: "Record left dirty across helper chain",
|
|
131
|
+
rootCause: `${caller.name} forwards ${binding.sourceVariableName} to ${callee.name}, which leaves the record in a Validate-dirty state on at least one exit path. ${caller.name} never persists after the call — the field write is silently discarded.`,
|
|
132
|
+
severity: "medium",
|
|
133
|
+
confidence: toConfidence([], "likely"),
|
|
134
|
+
primaryLocation: binding.argumentAnchor,
|
|
135
|
+
evidencePath: path,
|
|
136
|
+
affectedObjects: [caller.objectId, callee.objectId].sort(),
|
|
137
|
+
affectedTables: [],
|
|
138
|
+
fixOptions: [
|
|
139
|
+
{
|
|
140
|
+
description: `Add ${binding.sourceVariableName}.Modify() in ${caller.name} after the call to ${callee.name}, or have ${callee.name} persist before returning.`,
|
|
141
|
+
safety: "high",
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
provenance: [{ source: "tree-sitter" }],
|
|
145
|
+
};
|
|
146
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
147
|
+
findings.push(finding);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const sorted = findings.sort((a, b) => compareStrings(a.id, b.id));
|
|
153
|
+
return {
|
|
154
|
+
findings: sorted,
|
|
155
|
+
stats: {
|
|
156
|
+
detector: "d39-record-left-dirty-across-chain",
|
|
157
|
+
candidatesConsidered,
|
|
158
|
+
findingsEmitted: sorted.length,
|
|
159
|
+
skipped: {
|
|
160
|
+
...(skippedCallerPersists > 0 ? { callerPersists: skippedCallerPersists } : {}),
|
|
161
|
+
...(unknownDirtyCallee > 0 ? { unknownDirtyCallee } : {}),
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|