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,60 @@
|
|
|
1
|
+
import type { CapabilityConfidence, CapabilityFact, ValueSource } from "../../model/capability.ts";
|
|
2
|
+
import type { CoverageReason } from "../../model/coverage.ts";
|
|
3
|
+
import type { ExtractionContext } from "./extractor.ts";
|
|
4
|
+
import { classifyValueSource } from "./value-source.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Phase 0b-β hyperlink family extractor. Detects bare `Hyperlink(url)` calls.
|
|
8
|
+
*
|
|
9
|
+
* Emits: op="open", resourceKind="ui", resourceArgSource = classifyValueSource(urlArg),
|
|
10
|
+
* confidence derived from URL ValueSource kind. Never throws.
|
|
11
|
+
*/
|
|
12
|
+
export function extractHyperlink(ctx: ExtractionContext): {
|
|
13
|
+
facts: CapabilityFact[];
|
|
14
|
+
reasons: CoverageReason[];
|
|
15
|
+
} {
|
|
16
|
+
try {
|
|
17
|
+
const facts: CapabilityFact[] = [];
|
|
18
|
+
for (const cs of ctx.routine?.features?.callSites ?? []) {
|
|
19
|
+
const callee = cs.callee;
|
|
20
|
+
if (!callee || callee.kind !== "bare") continue;
|
|
21
|
+
if (typeof callee.name !== "string") continue;
|
|
22
|
+
if (callee.name.toLowerCase() !== "hyperlink") continue;
|
|
23
|
+
|
|
24
|
+
const urlInfo = cs.argumentInfos?.[0];
|
|
25
|
+
const urlSource: ValueSource =
|
|
26
|
+
urlInfo !== undefined ? classifyValueSource(urlInfo, ctx) : { kind: "unknown" };
|
|
27
|
+
|
|
28
|
+
facts.push({
|
|
29
|
+
subject: ctx.routine.id,
|
|
30
|
+
op: "open",
|
|
31
|
+
resourceKind: "ui",
|
|
32
|
+
resourceArgSource: urlSource,
|
|
33
|
+
confidence: confidenceFromSource(urlSource),
|
|
34
|
+
provenance: "direct",
|
|
35
|
+
via: "self",
|
|
36
|
+
witnessCallsiteId: cs.id,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return { facts, reasons: [] };
|
|
40
|
+
} catch {
|
|
41
|
+
return { facts: [], reasons: ["extraction-failed"] };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function confidenceFromSource(vs: ValueSource): CapabilityConfidence {
|
|
46
|
+
switch (vs.kind) {
|
|
47
|
+
case "literal":
|
|
48
|
+
case "enum":
|
|
49
|
+
return "static";
|
|
50
|
+
case "constant-var":
|
|
51
|
+
return confidenceFromSource(vs.initializer);
|
|
52
|
+
case "parameter":
|
|
53
|
+
return "userDynamic";
|
|
54
|
+
case "table-field":
|
|
55
|
+
return "configDynamic";
|
|
56
|
+
case "expression":
|
|
57
|
+
case "unknown":
|
|
58
|
+
return "unresolved";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CapabilityConfidence,
|
|
3
|
+
CapabilityFact,
|
|
4
|
+
CapabilityOp,
|
|
5
|
+
StorageExtra,
|
|
6
|
+
ValueSource,
|
|
7
|
+
} from "../../model/capability.ts";
|
|
8
|
+
import type { CoverageReason } from "../../model/coverage.ts";
|
|
9
|
+
import type { CallsiteId, OperationId } from "../../model/ids.ts";
|
|
10
|
+
import type { ExtractionContext } from "./extractor.ts";
|
|
11
|
+
import { classifyValueSource } from "./value-source.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Maps IsolatedStorage method names (lowercase) to their capability op.
|
|
15
|
+
* Methods not in this map are ignored.
|
|
16
|
+
*/
|
|
17
|
+
const ISOLATED_STORAGE_OPS = new Map<string, CapabilityOp>([
|
|
18
|
+
["get", "store-read"],
|
|
19
|
+
["getencrypted", "store-read"],
|
|
20
|
+
["contains", "store-read"],
|
|
21
|
+
["set", "store-write"],
|
|
22
|
+
["setencrypted", "store-write"],
|
|
23
|
+
["delete", "store-delete"],
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Maps a DataScope enum text (e.g. "DataScope::Company") to the StorageExtra
|
|
28
|
+
* scope literal. Falls back to "unknown" for unrecognized values.
|
|
29
|
+
*/
|
|
30
|
+
function parseDataScope(text: string): StorageExtra["scope"] {
|
|
31
|
+
const lower = text.toLowerCase();
|
|
32
|
+
if (lower.includes("::company")) return "Company";
|
|
33
|
+
if (lower.includes("::user")) return "User";
|
|
34
|
+
if (lower.includes("::module")) return "Module";
|
|
35
|
+
return "unknown";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildStorageFact(
|
|
39
|
+
ctx: ExtractionContext,
|
|
40
|
+
op: CapabilityOp,
|
|
41
|
+
keySource: ValueSource,
|
|
42
|
+
extra: StorageExtra,
|
|
43
|
+
witness: { kind: "operation"; id: OperationId } | { kind: "callsite"; id: CallsiteId },
|
|
44
|
+
): CapabilityFact {
|
|
45
|
+
const witnessFields =
|
|
46
|
+
witness.kind === "operation"
|
|
47
|
+
? { witnessOperationId: witness.id }
|
|
48
|
+
: { witnessCallsiteId: witness.id };
|
|
49
|
+
return {
|
|
50
|
+
subject: ctx.routine.id,
|
|
51
|
+
op,
|
|
52
|
+
resourceKind: "isolated-storage",
|
|
53
|
+
resourceArgSource: keySource,
|
|
54
|
+
confidence: confidenceFromSource(keySource),
|
|
55
|
+
provenance: "direct",
|
|
56
|
+
via: "self",
|
|
57
|
+
...witnessFields,
|
|
58
|
+
extra,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Phase 0b-β isolated-storage family extractor. Detects IsolatedStorage
|
|
64
|
+
* system reference method calls.
|
|
65
|
+
*
|
|
66
|
+
* IsolatedStorage is a SYSTEM REFERENCE — not a declared variable. The gate
|
|
67
|
+
* is `receiver.toLowerCase() === "isolatedstorage"` (member callee) or
|
|
68
|
+
* `recordVariableName.toLowerCase() === "isolatedstorage"` (recordOperation).
|
|
69
|
+
*
|
|
70
|
+
* Why both branches? `intraprocedural-ops.ts` RECORD_OP_MAP maps `.Get` and
|
|
71
|
+
* `.Delete` to RecordOperation regardless of receiver type — so these two
|
|
72
|
+
* methods land in `recordOperations`. The remaining methods (`.Set`,
|
|
73
|
+
* `.SetEncrypted`, `.GetEncrypted`, `.Contains`) land in `callSites` as
|
|
74
|
+
* member callees.
|
|
75
|
+
*
|
|
76
|
+
* Op mapping:
|
|
77
|
+
* .Get, .GetEncrypted, .Contains → store-read
|
|
78
|
+
* .Set, .SetEncrypted → store-write
|
|
79
|
+
* .Delete → store-delete
|
|
80
|
+
*
|
|
81
|
+
* `resourceArgSource` is classifyValueSource on the key argument (arg[0] for
|
|
82
|
+
* callSites, fieldArgumentInfos[0] for recordOperations). `.Delete` has no
|
|
83
|
+
* fieldArgumentInfos so key falls back to `{ kind: "unknown" }`.
|
|
84
|
+
*
|
|
85
|
+
* `StorageExtra` carries:
|
|
86
|
+
* keyArgSource — same as resourceArgSource
|
|
87
|
+
* valueArgSource — arg[1] for .Set/.SetEncrypted (callSites only)
|
|
88
|
+
* scope — arg[2] for .Set/.SetEncrypted; parsed from DataScope enum text
|
|
89
|
+
*
|
|
90
|
+
* Never throws.
|
|
91
|
+
*/
|
|
92
|
+
export function extractIsolatedStorage(ctx: ExtractionContext): {
|
|
93
|
+
facts: CapabilityFact[];
|
|
94
|
+
reasons: CoverageReason[];
|
|
95
|
+
} {
|
|
96
|
+
try {
|
|
97
|
+
const facts: CapabilityFact[] = [];
|
|
98
|
+
|
|
99
|
+
// Branch A: IsolatedStorage.Get / .Delete from recordOperations.
|
|
100
|
+
// intraprocedural-ops.ts recognises Get/Delete via RECORD_OP_MAP
|
|
101
|
+
// regardless of receiver type — filter by recordVariableName here.
|
|
102
|
+
for (const ro of ctx.routine?.features?.recordOperations ?? []) {
|
|
103
|
+
const recv = ro.recordVariableName;
|
|
104
|
+
if (typeof recv !== "string") continue;
|
|
105
|
+
if (recv.toLowerCase() !== "isolatedstorage") continue;
|
|
106
|
+
|
|
107
|
+
const method = ro.op.toLowerCase();
|
|
108
|
+
const op = ISOLATED_STORAGE_OPS.get(method);
|
|
109
|
+
if (op === undefined) continue;
|
|
110
|
+
|
|
111
|
+
const keyInfo = ro.fieldArgumentInfos?.[0];
|
|
112
|
+
const keySource: ValueSource =
|
|
113
|
+
keyInfo !== undefined ? classifyValueSource(keyInfo, ctx) : { kind: "unknown" };
|
|
114
|
+
|
|
115
|
+
const extra: StorageExtra = {
|
|
116
|
+
kind: "storage",
|
|
117
|
+
keyArgSource: keySource,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
facts.push(buildStorageFact(ctx, op, keySource, extra, { kind: "operation", id: ro.id }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Branch B: IsolatedStorage.Set / .SetEncrypted / .GetEncrypted / .Contains
|
|
124
|
+
// from callSites (member callees not caught by RECORD_OP_MAP).
|
|
125
|
+
for (const cs of ctx.routine?.features?.callSites ?? []) {
|
|
126
|
+
const callee = cs.callee;
|
|
127
|
+
if (!callee || callee.kind !== "member") continue;
|
|
128
|
+
if (callee.receiver.toLowerCase() !== "isolatedstorage") continue;
|
|
129
|
+
|
|
130
|
+
const method = callee.method.toLowerCase();
|
|
131
|
+
const op = ISOLATED_STORAGE_OPS.get(method);
|
|
132
|
+
if (op === undefined) continue;
|
|
133
|
+
|
|
134
|
+
const keyInfo = cs.argumentInfos?.[0];
|
|
135
|
+
const keySource: ValueSource =
|
|
136
|
+
keyInfo !== undefined ? classifyValueSource(keyInfo, ctx) : { kind: "unknown" };
|
|
137
|
+
|
|
138
|
+
const extra: StorageExtra = {
|
|
139
|
+
kind: "storage",
|
|
140
|
+
keyArgSource: keySource,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// For write methods: capture value arg (arg[1]) and scope arg (arg[2]).
|
|
144
|
+
if (op === "store-write") {
|
|
145
|
+
const valueInfo = cs.argumentInfos?.[1];
|
|
146
|
+
if (valueInfo !== undefined) {
|
|
147
|
+
extra.valueArgSource = classifyValueSource(valueInfo, ctx);
|
|
148
|
+
}
|
|
149
|
+
const scopeInfo = cs.argumentInfos?.[2];
|
|
150
|
+
if (scopeInfo !== undefined) {
|
|
151
|
+
extra.scope = parseDataScope(scopeInfo.text ?? "");
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
facts.push(buildStorageFact(ctx, op, keySource, extra, { kind: "callsite", id: cs.id }));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { facts, reasons: [] };
|
|
159
|
+
} catch {
|
|
160
|
+
return { facts: [], reasons: ["extraction-failed"] };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function confidenceFromSource(vs: ValueSource): CapabilityConfidence {
|
|
165
|
+
switch (vs.kind) {
|
|
166
|
+
case "literal":
|
|
167
|
+
case "enum":
|
|
168
|
+
return "static";
|
|
169
|
+
case "constant-var":
|
|
170
|
+
return confidenceFromSource(vs.initializer);
|
|
171
|
+
case "parameter":
|
|
172
|
+
return "userDynamic";
|
|
173
|
+
case "table-field":
|
|
174
|
+
return "configDynamic";
|
|
175
|
+
case "expression":
|
|
176
|
+
case "unknown":
|
|
177
|
+
return "unresolved";
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { CapabilityFact, CapabilityOp, TableExtra } from "../../model/capability.ts";
|
|
2
|
+
import type { CoverageReason } from "../../model/coverage.ts";
|
|
3
|
+
import type { RecordOpType } from "../../model/entities.ts";
|
|
4
|
+
import type { ExtractionContext } from "./extractor.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Map an AL RecordOpType to a CapabilityOp.
|
|
8
|
+
*
|
|
9
|
+
* Returns undefined for state-only / filter ops (SetRange, SetFilter, Init,
|
|
10
|
+
* SetLoadFields, AddLoadFields, SetCurrentKey, Reset, LockTable) — these are
|
|
11
|
+
* NOT capability-relevant per spec §3.1 substrate-discipline. Phase 1
|
|
12
|
+
* detectors that need filter / load-field state read RecordOperation directly.
|
|
13
|
+
*/
|
|
14
|
+
function mapOp(op: RecordOpType): CapabilityOp | undefined {
|
|
15
|
+
switch (op) {
|
|
16
|
+
case "Get":
|
|
17
|
+
case "Find":
|
|
18
|
+
case "FindFirst":
|
|
19
|
+
case "FindLast":
|
|
20
|
+
case "FindSet":
|
|
21
|
+
case "IsEmpty":
|
|
22
|
+
case "Count":
|
|
23
|
+
case "CountApprox":
|
|
24
|
+
case "Next":
|
|
25
|
+
case "CalcFields":
|
|
26
|
+
case "CalcSums":
|
|
27
|
+
case "TestField":
|
|
28
|
+
return "read";
|
|
29
|
+
case "Modify":
|
|
30
|
+
case "ModifyAll":
|
|
31
|
+
case "Validate":
|
|
32
|
+
case "Copy":
|
|
33
|
+
case "TransferFields":
|
|
34
|
+
return "modify";
|
|
35
|
+
case "Insert":
|
|
36
|
+
return "insert";
|
|
37
|
+
case "Delete":
|
|
38
|
+
case "DeleteAll":
|
|
39
|
+
return "delete";
|
|
40
|
+
case "Init":
|
|
41
|
+
case "SetRange":
|
|
42
|
+
case "SetFilter":
|
|
43
|
+
case "SetLoadFields":
|
|
44
|
+
case "AddLoadFields":
|
|
45
|
+
case "SetCurrentKey":
|
|
46
|
+
case "Reset":
|
|
47
|
+
case "LockTable":
|
|
48
|
+
return undefined;
|
|
49
|
+
default: {
|
|
50
|
+
// Exhaustiveness guard — adding a RecordOpType variant must extend
|
|
51
|
+
// this switch. The `never` cast triggers tsc if a new variant slips in.
|
|
52
|
+
const _exhaustive: never = op;
|
|
53
|
+
return _exhaustive;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Phase 0b-β table family extractor.
|
|
60
|
+
*
|
|
61
|
+
* Iterates `ctx.routine.features.recordOperations` (already indexed by L2)
|
|
62
|
+
* and maps each RecordOperation to a CapabilityFact. State-only ops are
|
|
63
|
+
* skipped (undefined mapOp result).
|
|
64
|
+
*
|
|
65
|
+
* Confidence:
|
|
66
|
+
* - "static" when tableId is present (resolved from .app symbol deps)
|
|
67
|
+
* - "unresolved" when tableId is absent (pure workspace, no dep resolution)
|
|
68
|
+
*
|
|
69
|
+
* TableExtra carries opSubtype, recordVariableId, and tempState — per spec
|
|
70
|
+
* §3.1, fieldArguments are NOT mirrored into extra (they stay on RecordOperation
|
|
71
|
+
* for Phase 1 detectors to read directly).
|
|
72
|
+
*
|
|
73
|
+
* Never throws.
|
|
74
|
+
*/
|
|
75
|
+
export function extractTable(ctx: ExtractionContext): {
|
|
76
|
+
facts: CapabilityFact[];
|
|
77
|
+
reasons: CoverageReason[];
|
|
78
|
+
} {
|
|
79
|
+
try {
|
|
80
|
+
const facts: CapabilityFact[] = [];
|
|
81
|
+
const recordOps = ctx.routine?.features?.recordOperations ?? [];
|
|
82
|
+
|
|
83
|
+
for (const op of recordOps) {
|
|
84
|
+
const capOp = mapOp(op.op);
|
|
85
|
+
if (capOp === undefined) continue;
|
|
86
|
+
|
|
87
|
+
const extra: TableExtra = {
|
|
88
|
+
kind: "table",
|
|
89
|
+
opSubtype: op.op,
|
|
90
|
+
recordVariableId: op.recordVariableId,
|
|
91
|
+
tempState: op.tempState,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const fact: CapabilityFact = {
|
|
95
|
+
subject: ctx.routine.id,
|
|
96
|
+
op: capOp,
|
|
97
|
+
resourceKind: "table",
|
|
98
|
+
resourceId: op.tableId,
|
|
99
|
+
confidence: op.tableId !== undefined ? "static" : "unresolved",
|
|
100
|
+
provenance: "direct",
|
|
101
|
+
via: "self",
|
|
102
|
+
witnessOperationId: op.id,
|
|
103
|
+
extra,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
facts.push(fact);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { facts, reasons: [] };
|
|
110
|
+
} catch {
|
|
111
|
+
return { facts: [], reasons: ["extraction-failed"] };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { CapabilityConfidence, CapabilityFact, ValueSource } from "../../model/capability.ts";
|
|
2
|
+
import type { CoverageReason } from "../../model/coverage.ts";
|
|
3
|
+
import type { ExtractionContext } from "./extractor.ts";
|
|
4
|
+
import { classifyValueSource } from "./value-source.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Phase 0b-β telemetry family extractor. Detects:
|
|
8
|
+
* - `Session.LogMessage(eventId, message, ...)` — member callee, receiver = "Session"
|
|
9
|
+
* - `LogMessage(eventId, message, ...)` — bare callee
|
|
10
|
+
*
|
|
11
|
+
* Emits one CapabilityFact per match:
|
|
12
|
+
* op: "log", resourceKind: "telemetry"
|
|
13
|
+
* resourceArgSource: classifyValueSource on eventId (1st positional arg)
|
|
14
|
+
* confidence derived from eventId ValueSource kind (same pattern as http.ts)
|
|
15
|
+
* provenance: "direct", via: "self"
|
|
16
|
+
* witnessCallsiteId: cs.id (both bare + member calls land in callSites)
|
|
17
|
+
*
|
|
18
|
+
* No TelemetryExtra defined in src/model/capability.ts — `extra` is omitted.
|
|
19
|
+
*
|
|
20
|
+
* Never throws.
|
|
21
|
+
*/
|
|
22
|
+
export function extractTelemetry(ctx: ExtractionContext): {
|
|
23
|
+
facts: CapabilityFact[];
|
|
24
|
+
reasons: CoverageReason[];
|
|
25
|
+
} {
|
|
26
|
+
try {
|
|
27
|
+
const facts: CapabilityFact[] = [];
|
|
28
|
+
for (const cs of ctx.routine?.features?.callSites ?? []) {
|
|
29
|
+
const callee = cs.callee;
|
|
30
|
+
if (!callee) continue;
|
|
31
|
+
|
|
32
|
+
let matches = false;
|
|
33
|
+
if (callee.kind === "member") {
|
|
34
|
+
// Session.LogMessage — receiver name compared case-insensitively
|
|
35
|
+
if (
|
|
36
|
+
callee.receiver.toLowerCase() === "session" &&
|
|
37
|
+
callee.method.toLowerCase() === "logmessage"
|
|
38
|
+
) {
|
|
39
|
+
matches = true;
|
|
40
|
+
}
|
|
41
|
+
} else if (callee.kind === "bare") {
|
|
42
|
+
if (callee.name.toLowerCase() === "logmessage") {
|
|
43
|
+
matches = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!matches) continue;
|
|
47
|
+
|
|
48
|
+
const eventIdInfo = cs.argumentInfos?.[0];
|
|
49
|
+
const eventIdSource: ValueSource =
|
|
50
|
+
eventIdInfo !== undefined ? classifyValueSource(eventIdInfo, ctx) : { kind: "unknown" };
|
|
51
|
+
|
|
52
|
+
facts.push({
|
|
53
|
+
subject: ctx.routine.id,
|
|
54
|
+
op: "log",
|
|
55
|
+
resourceKind: "telemetry",
|
|
56
|
+
resourceArgSource: eventIdSource,
|
|
57
|
+
confidence: confidenceFromSource(eventIdSource),
|
|
58
|
+
provenance: "direct",
|
|
59
|
+
via: "self",
|
|
60
|
+
witnessCallsiteId: cs.id,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return { facts, reasons: [] };
|
|
64
|
+
} catch {
|
|
65
|
+
return { facts: [], reasons: ["extraction-failed"] };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function confidenceFromSource(vs: ValueSource): CapabilityConfidence {
|
|
70
|
+
switch (vs.kind) {
|
|
71
|
+
case "literal":
|
|
72
|
+
case "enum":
|
|
73
|
+
return "static";
|
|
74
|
+
case "constant-var":
|
|
75
|
+
return confidenceFromSource(vs.initializer);
|
|
76
|
+
case "parameter":
|
|
77
|
+
return "userDynamic";
|
|
78
|
+
case "table-field":
|
|
79
|
+
return "configDynamic";
|
|
80
|
+
case "expression":
|
|
81
|
+
case "unknown":
|
|
82
|
+
return "unresolved";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CapabilityFact, CapabilityOp } from "../../model/capability.ts";
|
|
2
|
+
import type { CoverageReason } from "../../model/coverage.ts";
|
|
3
|
+
import type { ExtractionContext } from "./extractor.ts";
|
|
4
|
+
|
|
5
|
+
const UI_PRIMITIVES: Record<
|
|
6
|
+
string,
|
|
7
|
+
Extract<CapabilityOp, "ui-confirm" | "ui-message" | "ui-error">
|
|
8
|
+
> = {
|
|
9
|
+
confirm: "ui-confirm",
|
|
10
|
+
message: "ui-message",
|
|
11
|
+
error: "ui-error",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Phase 0b-β ui family extractor. Detects bare UI primitive calls:
|
|
16
|
+
* Confirm(...) → ui-confirm
|
|
17
|
+
* Message(...) → ui-message
|
|
18
|
+
* Error(...) → ui-error
|
|
19
|
+
*
|
|
20
|
+
* Presence facts — no resourceArgSource (message text isn't a resource id),
|
|
21
|
+
* no extra. Confidence always "static" (the primitive itself is the resource).
|
|
22
|
+
*
|
|
23
|
+
* Note: Error is already handled by D20 for control-flow semantics; this
|
|
24
|
+
* UI extraction is orthogonal — UI capability is independent of CFG semantics.
|
|
25
|
+
*
|
|
26
|
+
* Never throws.
|
|
27
|
+
*/
|
|
28
|
+
export function extractUi(ctx: ExtractionContext): {
|
|
29
|
+
facts: CapabilityFact[];
|
|
30
|
+
reasons: CoverageReason[];
|
|
31
|
+
} {
|
|
32
|
+
try {
|
|
33
|
+
const facts: CapabilityFact[] = [];
|
|
34
|
+
for (const cs of ctx.routine?.features?.callSites ?? []) {
|
|
35
|
+
const callee = cs.callee;
|
|
36
|
+
if (!callee || callee.kind !== "bare") continue;
|
|
37
|
+
if (typeof callee.name !== "string") continue;
|
|
38
|
+
const op = UI_PRIMITIVES[callee.name.toLowerCase()];
|
|
39
|
+
if (op === undefined) continue;
|
|
40
|
+
|
|
41
|
+
facts.push({
|
|
42
|
+
subject: ctx.routine.id,
|
|
43
|
+
op,
|
|
44
|
+
resourceKind: "ui",
|
|
45
|
+
confidence: "static",
|
|
46
|
+
provenance: "direct",
|
|
47
|
+
via: "self",
|
|
48
|
+
witnessCallsiteId: cs.id,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return { facts, reasons: [] };
|
|
52
|
+
} catch {
|
|
53
|
+
return { facts: [], reasons: ["extraction-failed"] };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// src/index/capability/value-source.ts
|
|
2
|
+
//
|
|
3
|
+
// Classify an `ExpressionInfo` (a serialized, tree-sitter-derived expression
|
|
4
|
+
// summary) into a `ValueSource`. Used by every capability family extractor when
|
|
5
|
+
// capturing resource arguments: URL for HTTP, key for IsolatedStorage, target id
|
|
6
|
+
// for object-run dispatch, etc.
|
|
7
|
+
//
|
|
8
|
+
// Chases `constant-var` chains by looking up the identifier name in
|
|
9
|
+
// `ctx.variables` and inspecting the `VariableSymbol.initializer` captured by
|
|
10
|
+
// Phase 0b-α's `extractInitializer`. Chase depth is capped at MAX_CHASE_DEPTH
|
|
11
|
+
// (3) to prevent infinite recursion on circular initializer chains.
|
|
12
|
+
//
|
|
13
|
+
// MUST NEVER throw — the outer `try/catch` in `classifyValueSource` returns
|
|
14
|
+
// `{kind: "unknown"}` on any internal error, per the engine-never-throws
|
|
15
|
+
// contract (CLAUDE.md §"The engine never throws").
|
|
16
|
+
|
|
17
|
+
import type { ValueSource } from "../../model/capability.ts";
|
|
18
|
+
import type { ExpressionInfo } from "../../model/expression.ts";
|
|
19
|
+
import type { ExtractionContext } from "./extractor.ts";
|
|
20
|
+
|
|
21
|
+
const MAX_CHASE_DEPTH = 3;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Classify an `ExpressionInfo` as a `ValueSource`. Phase 0b-β capability
|
|
25
|
+
* extractors call this for every resource argument (HTTP URL, IsolatedStorage
|
|
26
|
+
* key, dispatch target id, etc.) so downstream consumers can reason about
|
|
27
|
+
* provenance (literal vs config-table vs user-input).
|
|
28
|
+
*
|
|
29
|
+
* Pass `null` or `undefined` to get `{kind: "unknown"}` — never throws.
|
|
30
|
+
*/
|
|
31
|
+
export function classifyValueSource(
|
|
32
|
+
info: ExpressionInfo | null | undefined,
|
|
33
|
+
ctx: ExtractionContext,
|
|
34
|
+
): ValueSource {
|
|
35
|
+
try {
|
|
36
|
+
return classifyAtDepth(info, ctx, 0);
|
|
37
|
+
} catch {
|
|
38
|
+
return { kind: "unknown" };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function classifyAtDepth(
|
|
43
|
+
info: ExpressionInfo | null | undefined,
|
|
44
|
+
ctx: ExtractionContext,
|
|
45
|
+
depth: number,
|
|
46
|
+
): ValueSource {
|
|
47
|
+
if (info === null || info === undefined) {
|
|
48
|
+
return { kind: "unknown" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
switch (info.kind) {
|
|
52
|
+
// ── Literal forms ──────────────────────────────────────────────────────
|
|
53
|
+
case "string_literal":
|
|
54
|
+
// `info.value` is the content between the single quotes (set by
|
|
55
|
+
// expressionInfoFromNode / deriveParts for string_literal).
|
|
56
|
+
return { kind: "literal", value: info.value ?? stripSingleQuotes(info.text) };
|
|
57
|
+
|
|
58
|
+
case "integer":
|
|
59
|
+
case "decimal":
|
|
60
|
+
case "boolean":
|
|
61
|
+
return { kind: "literal", value: info.value ?? info.text.trim() };
|
|
62
|
+
|
|
63
|
+
// ── Enum / database reference ─────────────────────────────────────────
|
|
64
|
+
// Both `qualified_enum_value` (e.g. `MyEnum::Value`) and
|
|
65
|
+
// `database_reference` (e.g. `Codeunit::"Job Q Codeunit"`) have the
|
|
66
|
+
// same shape: qualifier = LHS of `::`, member/value = RHS. Both are
|
|
67
|
+
// statically-known references, so both map to ValueSource "enum".
|
|
68
|
+
case "qualified_enum_value":
|
|
69
|
+
case "database_reference": {
|
|
70
|
+
const enumName = info.qualifier !== undefined ? stripDoubleQuotes(info.qualifier) : "";
|
|
71
|
+
const member = info.member ?? info.value ?? "";
|
|
72
|
+
return { kind: "enum", enumName, member };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Identifier — parameter, constant-var, or chase ────────────────────
|
|
76
|
+
case "identifier":
|
|
77
|
+
case "quoted_identifier": {
|
|
78
|
+
const name = info.value !== undefined ? info.value.toLowerCase() : info.text.toLowerCase();
|
|
79
|
+
return classifyIdentifier(name, ctx, depth);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Member expression — potential table-field ─────────────────────────
|
|
83
|
+
case "member_expression": {
|
|
84
|
+
return classifyMemberExpression(info.text, ctx);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Unary expression (e.g. -3) — literal when operand is numeric ──────
|
|
88
|
+
case "unary_expression":
|
|
89
|
+
if (info.value !== undefined) return { kind: "literal", value: info.value };
|
|
90
|
+
return { kind: "expression" };
|
|
91
|
+
|
|
92
|
+
// ── Call / anything else → expression ─────────────────────────────────
|
|
93
|
+
default:
|
|
94
|
+
return { kind: "expression" };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Identifier resolution ──────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function classifyIdentifier(nameLower: string, ctx: ExtractionContext, depth: number): ValueSource {
|
|
101
|
+
const sym = ctx.variables.get(nameLower);
|
|
102
|
+
if (sym === undefined) {
|
|
103
|
+
// Not in scope — treat as an opaque expression reference.
|
|
104
|
+
return { kind: "expression" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (sym.isParameter) {
|
|
108
|
+
return {
|
|
109
|
+
kind: "parameter",
|
|
110
|
+
index: sym.parameterIndex ?? 0,
|
|
111
|
+
varName: nameLower,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Local variable — see if we can chase the initializer.
|
|
116
|
+
const init = sym.initializer;
|
|
117
|
+
if (init === undefined || init.kind === "unknown" || init.kind === "expression") {
|
|
118
|
+
// No initializer captured or it's already opaque — emit constant-var.
|
|
119
|
+
return { kind: "constant-var", varName: nameLower, initializer: init ?? { kind: "unknown" } };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (depth >= MAX_CHASE_DEPTH) {
|
|
123
|
+
// Depth cap hit — return constant-var with the raw initializer but don't recurse.
|
|
124
|
+
return { kind: "constant-var", varName: nameLower, initializer: init };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Chase one hop deeper for `constant-var` (var-to-var alias).
|
|
128
|
+
if (init.kind === "constant-var") {
|
|
129
|
+
const deeper = classifyIdentifier(init.varName, ctx, depth + 1);
|
|
130
|
+
if (deeper.kind === "literal" || deeper.kind === "enum" || deeper.kind === "parameter") {
|
|
131
|
+
// Successfully resolved to a concrete source — return it directly so
|
|
132
|
+
// callers see the root kind rather than a chain of constant-var wrappers.
|
|
133
|
+
return deeper;
|
|
134
|
+
}
|
|
135
|
+
return { kind: "constant-var", varName: nameLower, initializer: deeper };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Initializer is already a resolved kind (literal / enum / parameter / table-field).
|
|
139
|
+
return init;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Member expression (potential table-field) ──────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parse a member-expression text of the form `Receiver.Field` or
|
|
146
|
+
* `Receiver."Quoted Field"` and classify it as a `table-field` when the
|
|
147
|
+
* receiver resolves to a record-typed variable with a known tableId.
|
|
148
|
+
* Falls back to `expression` when the receiver is not a known record variable.
|
|
149
|
+
*
|
|
150
|
+
* The text splitting relies on the first `.` being the separator between the
|
|
151
|
+
* receiver identifier and the field member. This is safe because:
|
|
152
|
+
* - AL receiver names are bare identifiers (no dots).
|
|
153
|
+
* - `member_expression` nodes in the grammar always have exactly one `.`
|
|
154
|
+
* separator at the top level.
|
|
155
|
+
*/
|
|
156
|
+
function classifyMemberExpression(text: string, ctx: ExtractionContext): ValueSource {
|
|
157
|
+
const dotIdx = text.indexOf(".");
|
|
158
|
+
if (dotIdx === -1) {
|
|
159
|
+
return { kind: "expression" };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const receiverRaw = text.slice(0, dotIdx).trim();
|
|
163
|
+
const fieldRaw = text.slice(dotIdx + 1).trim();
|
|
164
|
+
const receiverLower = receiverRaw.toLowerCase();
|
|
165
|
+
|
|
166
|
+
const sym = ctx.variables.get(receiverLower);
|
|
167
|
+
if (sym === undefined) {
|
|
168
|
+
return { kind: "expression" };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Is the receiver a record variable?
|
|
172
|
+
const declType = sym.declaredType.toLowerCase();
|
|
173
|
+
const isRecord =
|
|
174
|
+
declType.startsWith("record ") || declType === "record" || declType.startsWith("recordref");
|
|
175
|
+
|
|
176
|
+
if (!isRecord) {
|
|
177
|
+
// Member call on a non-record (e.g. HttpClient.Get) — expression.
|
|
178
|
+
return { kind: "expression" };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const fieldName = stripDoubleQuotes(fieldRaw);
|
|
182
|
+
const tableId = sym.tableId ?? "unknown";
|
|
183
|
+
return { kind: "table-field", tableId, fieldName };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Quote helpers ───────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
function stripSingleQuotes(s: string): string {
|
|
189
|
+
const t = s.trim();
|
|
190
|
+
if (t.length >= 2 && t[0] === "'" && t[t.length - 1] === "'") {
|
|
191
|
+
return t.slice(1, -1);
|
|
192
|
+
}
|
|
193
|
+
return t;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function stripDoubleQuotes(s: string): string {
|
|
197
|
+
const t = s.trim();
|
|
198
|
+
if (t.length >= 2 && t[0] === '"' && t[t.length - 1] === '"') {
|
|
199
|
+
return t.slice(1, -1);
|
|
200
|
+
}
|
|
201
|
+
return t;
|
|
202
|
+
}
|