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,253 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LoopNode,
|
|
3
|
+
LoopType,
|
|
4
|
+
OperationSite,
|
|
5
|
+
RecordOpType,
|
|
6
|
+
RecordOperation,
|
|
7
|
+
} from "../model/entities.ts";
|
|
8
|
+
import { type LoopId, type RoutineId, encodeLoopId, encodeOperationId } from "../model/ids.ts";
|
|
9
|
+
import { nodeToSourceRange } from "../parser/ast.ts";
|
|
10
|
+
import type { Node as SyntaxNode } from "../parser/native/wrapper.ts";
|
|
11
|
+
|
|
12
|
+
const LOOP_TYPE_MAP: Record<string, LoopType> = {
|
|
13
|
+
repeat_statement: "repeat",
|
|
14
|
+
for_statement: "for",
|
|
15
|
+
foreach_statement: "foreach",
|
|
16
|
+
while_statement: "while",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Canonical record-op name (lowercase) -> properly-cased RecordOpType.
|
|
21
|
+
* Confirmed against al-perf/src/source/indexer.ts RECORD_OP_CASE_MAP (same grammar).
|
|
22
|
+
*/
|
|
23
|
+
const RECORD_OP_MAP: Record<string, RecordOpType> = {
|
|
24
|
+
findset: "FindSet",
|
|
25
|
+
findfirst: "FindFirst",
|
|
26
|
+
findlast: "FindLast",
|
|
27
|
+
find: "Find",
|
|
28
|
+
get: "Get",
|
|
29
|
+
calcfields: "CalcFields",
|
|
30
|
+
calcsums: "CalcSums",
|
|
31
|
+
modify: "Modify",
|
|
32
|
+
modifyall: "ModifyAll",
|
|
33
|
+
insert: "Insert",
|
|
34
|
+
delete: "Delete",
|
|
35
|
+
deleteall: "DeleteAll",
|
|
36
|
+
setloadfields: "SetLoadFields",
|
|
37
|
+
addloadfields: "AddLoadFields",
|
|
38
|
+
setrange: "SetRange",
|
|
39
|
+
setfilter: "SetFilter",
|
|
40
|
+
setcurrentkey: "SetCurrentKey",
|
|
41
|
+
reset: "Reset",
|
|
42
|
+
copy: "Copy",
|
|
43
|
+
transferfields: "TransferFields",
|
|
44
|
+
validate: "Validate",
|
|
45
|
+
init: "Init",
|
|
46
|
+
next: "Next",
|
|
47
|
+
count: "Count",
|
|
48
|
+
countapprox: "CountApprox",
|
|
49
|
+
isempty: "IsEmpty",
|
|
50
|
+
locktable: "LockTable",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Record ops for which we capture all field arguments.
|
|
55
|
+
*
|
|
56
|
+
* Includes `Validate` so the summary engine's parameter-effects computation
|
|
57
|
+
* (summary-engine.ts: \`if (op.op === "Validate") for (arg of op.fieldArguments)\`)
|
|
58
|
+
* actually receives the field-name arguments — without this entry, fieldArguments
|
|
59
|
+
* was always undefined for Validate ops and the writesFields tracking was silently
|
|
60
|
+
* empty.
|
|
61
|
+
*
|
|
62
|
+
* Includes `Get`, `FindFirst`, `FindLast` so that D4 (repeated identical lookup in
|
|
63
|
+
* a loop) can read the first argument (the key expression) and detect duplicate calls
|
|
64
|
+
* with the same literal key. `Find` and `FindSet` are also included for completeness,
|
|
65
|
+
* even though D4 does not currently use them as lookup ops to deduplicate.
|
|
66
|
+
*/
|
|
67
|
+
const FIELD_ARGS_OPS = new Set([
|
|
68
|
+
"SetRange",
|
|
69
|
+
"SetFilter",
|
|
70
|
+
"SetLoadFields",
|
|
71
|
+
"AddLoadFields",
|
|
72
|
+
"SetCurrentKey",
|
|
73
|
+
"Validate",
|
|
74
|
+
"Get",
|
|
75
|
+
"Find",
|
|
76
|
+
"FindFirst",
|
|
77
|
+
"FindLast",
|
|
78
|
+
"FindSet",
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
export interface ExtractOpsResult {
|
|
82
|
+
loops: LoopNode[];
|
|
83
|
+
operationSites: OperationSite[];
|
|
84
|
+
recordOperations: RecordOperation[];
|
|
85
|
+
/**
|
|
86
|
+
* Next free operation index after this extractor's assignments. The routine indexer
|
|
87
|
+
* threads this into `extractRefs` as `startOpIndex` so call-site operation IDs do not
|
|
88
|
+
* collide with record-op IDs in the same routine.
|
|
89
|
+
*/
|
|
90
|
+
nextOpIndex: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Inspect a `call_expression` for member-call shape and check whether its method maps
|
|
95
|
+
* to a record op. Lazy by design: decode only what's needed at each step.
|
|
96
|
+
*
|
|
97
|
+
* CONFIRMED grammar (V2, same as al-perf/src/source/indexer.ts):
|
|
98
|
+
* - Call node type: `call_expression`
|
|
99
|
+
* - Callee field name: `"function"` (may be a `member_expression`)
|
|
100
|
+
* - Member object field: `"object"`
|
|
101
|
+
* - Member property field: `"member"`
|
|
102
|
+
* - Argument list: named child with type `"argument_list"`
|
|
103
|
+
*
|
|
104
|
+
* Returns null if not a member-call OR the method isn't a record op. Returning a
|
|
105
|
+
* decoded `receiver` and (when relevant) `args` deferred so non-record member calls —
|
|
106
|
+
* the overwhelming majority — pay nothing past the method-name lookup.
|
|
107
|
+
*/
|
|
108
|
+
function parseRecordMemberCall(
|
|
109
|
+
node: SyntaxNode,
|
|
110
|
+
): { receiver: string; opType: RecordOpType; args: string[] } | null {
|
|
111
|
+
const funcNode = node.childForFieldName("function") ?? node.namedChildren[0];
|
|
112
|
+
if (!funcNode) return null;
|
|
113
|
+
if (funcNode.type !== "member_expression") return null;
|
|
114
|
+
|
|
115
|
+
const propNode = funcNode.childForFieldName("member") ?? funcNode.namedChildren[1];
|
|
116
|
+
if (!propNode) return null;
|
|
117
|
+
|
|
118
|
+
// Decode the method name first — cheap (just the property identifier). Most member
|
|
119
|
+
// calls aren't record ops; bail before paying for receiver decode or arg traversal.
|
|
120
|
+
const opType = RECORD_OP_MAP[propNode.text.toLowerCase()];
|
|
121
|
+
if (opType === undefined) return null;
|
|
122
|
+
|
|
123
|
+
const objNode = funcNode.childForFieldName("object") ?? funcNode.namedChildren[0];
|
|
124
|
+
const receiver = objNode ? objNode.text : "";
|
|
125
|
+
|
|
126
|
+
// Argument decode is only meaningful for the field-arg ops; everything else gets [].
|
|
127
|
+
let args: string[] = [];
|
|
128
|
+
if (FIELD_ARGS_OPS.has(opType)) {
|
|
129
|
+
const argListNode = node.namedChildren.find((c) => c !== null && c.type === "argument_list");
|
|
130
|
+
if (argListNode) {
|
|
131
|
+
args = [];
|
|
132
|
+
for (const arg of argListNode.namedChildren) {
|
|
133
|
+
if (arg !== null) args.push(arg.text);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { receiver, opType, args };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Detect whether a `call_expression` node is a bare `Commit()` call.
|
|
143
|
+
*
|
|
144
|
+
* CONFIRMED grammar: the `function` child is an `identifier` node whose text is "Commit"
|
|
145
|
+
* (case-insensitive). Not a `member_expression`. Cheap shape check first; only decode
|
|
146
|
+
* `.text` when the function child is an identifier.
|
|
147
|
+
*/
|
|
148
|
+
function isBareCommitCall(node: SyntaxNode): boolean {
|
|
149
|
+
const funcNode = node.childForFieldName("function") ?? node.namedChildren[0];
|
|
150
|
+
if (!funcNode) return false;
|
|
151
|
+
if (funcNode.type !== "identifier") return false; // gates the text decode below
|
|
152
|
+
return funcNode.text.toLowerCase() === "commit";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Extract loops, operation sites, and record operations from a routine body node.
|
|
157
|
+
* `bodyNode` is typically the routine's `code_block`.
|
|
158
|
+
*
|
|
159
|
+
* Single DFS with maintained ancestor `loopStack`. The previous implementation did two
|
|
160
|
+
* full traversals (one to find/register loops, another to find ops and call
|
|
161
|
+
* `loopAncestorsOf` per call site — which walked `.parent` chains via FFI). This version
|
|
162
|
+
* cuts the FFI/allocation cost in half on routine bodies. Document-order preorder is
|
|
163
|
+
* preserved by recursing into named children left-to-right.
|
|
164
|
+
*/
|
|
165
|
+
export function extractOpsAndLoops(
|
|
166
|
+
bodyNode: SyntaxNode,
|
|
167
|
+
routineId: RoutineId,
|
|
168
|
+
sourceUnitId: string,
|
|
169
|
+
): ExtractOpsResult {
|
|
170
|
+
const loops: LoopNode[] = [];
|
|
171
|
+
const operationSites: OperationSite[] = [];
|
|
172
|
+
const recordOperations: RecordOperation[] = [];
|
|
173
|
+
let opIndex = 0;
|
|
174
|
+
const loopStack: LoopId[] = [];
|
|
175
|
+
|
|
176
|
+
function visit(node: SyntaxNode): void {
|
|
177
|
+
const nodeType = node.type;
|
|
178
|
+
const loopType = LOOP_TYPE_MAP[nodeType];
|
|
179
|
+
let pushedLoop = false;
|
|
180
|
+
if (loopType !== undefined) {
|
|
181
|
+
const id = encodeLoopId(routineId, loops.length);
|
|
182
|
+
loops.push({
|
|
183
|
+
id,
|
|
184
|
+
type: loopType,
|
|
185
|
+
sourceAnchor: {
|
|
186
|
+
sourceUnitId,
|
|
187
|
+
range: nodeToSourceRange(node),
|
|
188
|
+
enclosingRoutineId: routineId,
|
|
189
|
+
syntaxKind: nodeType,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
loopStack.push(id);
|
|
193
|
+
pushedLoop = true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Only call_expression nodes can be record-op member calls or bare Commit().
|
|
197
|
+
if (nodeType === "call_expression") {
|
|
198
|
+
const memberCall = parseRecordMemberCall(node);
|
|
199
|
+
if (memberCall !== null) {
|
|
200
|
+
const anchor = {
|
|
201
|
+
sourceUnitId,
|
|
202
|
+
range: nodeToSourceRange(node),
|
|
203
|
+
enclosingRoutineId: routineId,
|
|
204
|
+
syntaxKind: nodeType,
|
|
205
|
+
};
|
|
206
|
+
const opId = encodeOperationId(routineId, opIndex++);
|
|
207
|
+
// snapshot the ambient ancestor loop stack — naturally outermost-first
|
|
208
|
+
const snapshotLoopStack = loopStack.slice();
|
|
209
|
+
recordOperations.push({
|
|
210
|
+
id: opId,
|
|
211
|
+
routineId,
|
|
212
|
+
op: memberCall.opType,
|
|
213
|
+
recordVariableName: memberCall.receiver,
|
|
214
|
+
tempState: { kind: "unknown" },
|
|
215
|
+
fieldArguments: FIELD_ARGS_OPS.has(memberCall.opType) ? memberCall.args : undefined,
|
|
216
|
+
loopStack: snapshotLoopStack,
|
|
217
|
+
sourceAnchor: anchor,
|
|
218
|
+
});
|
|
219
|
+
operationSites.push({
|
|
220
|
+
id: opId,
|
|
221
|
+
routineId,
|
|
222
|
+
kind: memberCall.opType === "LockTable" ? "lock" : "record-op",
|
|
223
|
+
sourceAnchor: anchor,
|
|
224
|
+
loopStack: snapshotLoopStack,
|
|
225
|
+
});
|
|
226
|
+
} else if (isBareCommitCall(node)) {
|
|
227
|
+
const opId = encodeOperationId(routineId, opIndex++);
|
|
228
|
+
operationSites.push({
|
|
229
|
+
id: opId,
|
|
230
|
+
routineId,
|
|
231
|
+
kind: "commit",
|
|
232
|
+
sourceAnchor: {
|
|
233
|
+
sourceUnitId,
|
|
234
|
+
range: nodeToSourceRange(node),
|
|
235
|
+
enclosingRoutineId: routineId,
|
|
236
|
+
syntaxKind: nodeType,
|
|
237
|
+
},
|
|
238
|
+
loopStack: loopStack.slice(),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const child of node.namedChildren) {
|
|
244
|
+
if (child) visit(child);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (pushedLoop) loopStack.pop();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
visit(bodyNode);
|
|
251
|
+
|
|
252
|
+
return { loops, operationSites, recordOperations, nextOpIndex: opIndex };
|
|
253
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { ParameterSymbol, RecordVariable, TempState } from "../model/entities.ts";
|
|
2
|
+
import { type RoutineId, encodeRecordVariableId } from "../model/ids.ts";
|
|
3
|
+
import { stripQuotes } from "../parser/ast.ts";
|
|
4
|
+
import type { Node as SyntaxNode } from "../parser/native/wrapper.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extract record variables from the parameter list and var_section of a routine node.
|
|
8
|
+
*
|
|
9
|
+
* CONFIRMED grammar (V2):
|
|
10
|
+
* - Parameters: `parameter_list` → `parameter` children.
|
|
11
|
+
* Each parameter has: optional `var_keyword`, `identifier` (name), `type_specification`
|
|
12
|
+
* - Variables: `var_section` → `variable_declaration` children.
|
|
13
|
+
* Each has: `identifier` (name), `type_specification` → `record_type`
|
|
14
|
+
* - `record_type` children: `quoted_identifier` | `identifier` (table name),
|
|
15
|
+
* optionally `temporary_keyword`
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Public re-export so the routine indexer can run record-variable + parameter extraction
|
|
19
|
+
* up front (before the merged body walk needs `recordVarNames`).
|
|
20
|
+
*
|
|
21
|
+
* Two-phase signature so the canonical routine signature can be hashed from
|
|
22
|
+
* structured `parameters` BEFORE the routineId is encoded. `extractParameters`
|
|
23
|
+
* needs no routineId; `extractRecordVariables` takes the final routineId so
|
|
24
|
+
* `RecordVariable.id` values are correct on first construction.
|
|
25
|
+
*/
|
|
26
|
+
export function extractRecordVariablesAndParameters(
|
|
27
|
+
procNode: SyntaxNode,
|
|
28
|
+
routineId: RoutineId,
|
|
29
|
+
): { recordVariables: RecordVariable[]; parameters: ParameterSymbol[] } {
|
|
30
|
+
const parameters = extractParameters(procNode);
|
|
31
|
+
const recordVariables = extractRecordVariables(procNode, routineId, parameters);
|
|
32
|
+
return { recordVariables, parameters };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract `ParameterSymbol[]` from a procedure/trigger node's `parameter_list`.
|
|
37
|
+
* No routineId required — parameters carry only structural data, not ids.
|
|
38
|
+
*/
|
|
39
|
+
export function extractParameters(procNode: SyntaxNode): ParameterSymbol[] {
|
|
40
|
+
const parameters: ParameterSymbol[] = [];
|
|
41
|
+
const paramList = procNode.namedChildren.find((c) => c !== null && c.type === "parameter_list");
|
|
42
|
+
if (!paramList) return parameters;
|
|
43
|
+
let pIndex = 0;
|
|
44
|
+
for (const param of paramList.namedChildren) {
|
|
45
|
+
if (!param || param.type !== "parameter") continue;
|
|
46
|
+
const isVar = param.namedChildren.some((c) => c !== null && c.type === "var_keyword");
|
|
47
|
+
const nameNode = param.namedChildren.find((c) => c !== null && c.type === "identifier");
|
|
48
|
+
const typeSpecNode = param.namedChildren.find(
|
|
49
|
+
(c) => c !== null && c.type === "type_specification",
|
|
50
|
+
);
|
|
51
|
+
if (!nameNode) {
|
|
52
|
+
pIndex++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const name = nameNode.text;
|
|
56
|
+
const typeText = typeSpecNode?.text ?? "";
|
|
57
|
+
const recordTypeNode = typeSpecNode?.namedChildren.find(
|
|
58
|
+
(c) => c !== null && c.type === "record_type",
|
|
59
|
+
);
|
|
60
|
+
const isRecord = recordTypeNode !== undefined;
|
|
61
|
+
let tableName: string | undefined;
|
|
62
|
+
if (isRecord && recordTypeNode) {
|
|
63
|
+
const quotedId = recordTypeNode.namedChildren.find(
|
|
64
|
+
(c) => c !== null && c.type === "quoted_identifier",
|
|
65
|
+
);
|
|
66
|
+
if (quotedId) tableName = stripQuotes(quotedId.text);
|
|
67
|
+
else {
|
|
68
|
+
const id = recordTypeNode.namedChildren.find((c) => c !== null && c.type === "identifier");
|
|
69
|
+
if (id) tableName = id.text;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
parameters.push({
|
|
73
|
+
index: pIndex++,
|
|
74
|
+
name,
|
|
75
|
+
typeText,
|
|
76
|
+
isVar,
|
|
77
|
+
isRecord,
|
|
78
|
+
tableName,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return parameters;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extractRecordVariables(
|
|
85
|
+
procNode: SyntaxNode,
|
|
86
|
+
routineId: RoutineId,
|
|
87
|
+
parameters: ParameterSymbol[],
|
|
88
|
+
): RecordVariable[] {
|
|
89
|
+
const recordVariables: RecordVariable[] = [];
|
|
90
|
+
|
|
91
|
+
// --- Record-typed parameters ---
|
|
92
|
+
// Walk the `parameter_list` again to recover the `record_type` node — needed for
|
|
93
|
+
// the `temporary_keyword` check below. The structural `parameters` array tells us
|
|
94
|
+
// which positional slot maps to which name; the node walk supplies the temporary
|
|
95
|
+
// flag the structural extractor doesn't carry.
|
|
96
|
+
const paramList = procNode.namedChildren.find((c) => c !== null && c.type === "parameter_list");
|
|
97
|
+
if (paramList) {
|
|
98
|
+
let pIndex = -1;
|
|
99
|
+
for (const param of paramList.namedChildren) {
|
|
100
|
+
if (!param || param.type !== "parameter") continue;
|
|
101
|
+
pIndex++;
|
|
102
|
+
const meta = parameters[pIndex];
|
|
103
|
+
if (meta === undefined || !meta.isRecord) continue;
|
|
104
|
+
|
|
105
|
+
const typeSpecNode = param.namedChildren.find(
|
|
106
|
+
(c) => c !== null && c.type === "type_specification",
|
|
107
|
+
);
|
|
108
|
+
const recordTypeNode = typeSpecNode?.namedChildren.find(
|
|
109
|
+
(c) => c !== null && c.type === "record_type",
|
|
110
|
+
);
|
|
111
|
+
if (!recordTypeNode) continue;
|
|
112
|
+
|
|
113
|
+
// Inspect the parameter's record type for the `temporary` keyword.
|
|
114
|
+
// AL enforces at compile time that a `temporary` parameter can only
|
|
115
|
+
// be bound to a temporary record at the callsite — so a parameter
|
|
116
|
+
// carrying the keyword is known-temp regardless of var-ness. Without
|
|
117
|
+
// this, D40 false-positives on every temp-record forwarding chain
|
|
118
|
+
// (e.g. `Sort(TempBusinessEntity)` where both sides declare
|
|
119
|
+
// `temporary` on the parameter).
|
|
120
|
+
const isTemporaryParam = recordTypeNode.namedChildren.some(
|
|
121
|
+
(c) => c !== null && c.type === "temporary_keyword",
|
|
122
|
+
);
|
|
123
|
+
// Without the keyword:
|
|
124
|
+
// By-var: temp-ness depends on what caller passes in.
|
|
125
|
+
// By-value: local copy — known non-temp.
|
|
126
|
+
const tempState: TempState = isTemporaryParam
|
|
127
|
+
? { kind: "known", value: true }
|
|
128
|
+
: meta.isVar
|
|
129
|
+
? { kind: "parameter-dependent", parameterIndex: meta.index }
|
|
130
|
+
: { kind: "known", value: false };
|
|
131
|
+
recordVariables.push({
|
|
132
|
+
id: encodeRecordVariableId(routineId, meta.name),
|
|
133
|
+
name: meta.name,
|
|
134
|
+
tableName: meta.tableName,
|
|
135
|
+
tempState,
|
|
136
|
+
isParameter: true,
|
|
137
|
+
parameterIndex: meta.index,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- Local variable declarations ---
|
|
143
|
+
const varSection = procNode.namedChildren.find((c) => c !== null && c.type === "var_section");
|
|
144
|
+
if (varSection) {
|
|
145
|
+
for (const varDecl of varSection.namedChildren) {
|
|
146
|
+
if (!varDecl || varDecl.type !== "variable_declaration") continue;
|
|
147
|
+
|
|
148
|
+
const nameNode = varDecl.namedChildren.find((c) => c !== null && c.type === "identifier");
|
|
149
|
+
const typeSpecNode = varDecl.namedChildren.find(
|
|
150
|
+
(c) => c !== null && c.type === "type_specification",
|
|
151
|
+
);
|
|
152
|
+
if (!nameNode || !typeSpecNode) continue;
|
|
153
|
+
|
|
154
|
+
const recordTypeNode = typeSpecNode.namedChildren.find(
|
|
155
|
+
(c) => c !== null && c.type === "record_type",
|
|
156
|
+
);
|
|
157
|
+
if (!recordTypeNode) continue; // not a Record variable
|
|
158
|
+
|
|
159
|
+
const name = nameNode.text;
|
|
160
|
+
|
|
161
|
+
// Resolve table name
|
|
162
|
+
let tableName: string | undefined;
|
|
163
|
+
const quotedId = recordTypeNode.namedChildren.find(
|
|
164
|
+
(c) => c !== null && c.type === "quoted_identifier",
|
|
165
|
+
);
|
|
166
|
+
if (quotedId) {
|
|
167
|
+
tableName = stripQuotes(quotedId.text);
|
|
168
|
+
} else {
|
|
169
|
+
const id = recordTypeNode.namedChildren.find((c) => c !== null && c.type === "identifier");
|
|
170
|
+
if (id) tableName = id.text;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const isTemporary = recordTypeNode.namedChildren.some(
|
|
174
|
+
(c) => c !== null && c.type === "temporary_keyword",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
recordVariables.push({
|
|
178
|
+
id: encodeRecordVariableId(routineId, name),
|
|
179
|
+
name,
|
|
180
|
+
tableName,
|
|
181
|
+
tempState: { kind: "known", value: isTemporary },
|
|
182
|
+
isParameter: false,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return recordVariables;
|
|
188
|
+
}
|