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,117 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { join, relative, sep } from "node:path";
|
|
4
|
+
import { sha256OfStrings } from "../hash.ts";
|
|
5
|
+
import type { AppIdentity } from "../model/identity.ts";
|
|
6
|
+
import type { ProviderDiagnostic, ProviderResult, SourceProvider, SourceUnit } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Directory names the walk skips. Kept deliberately minimal: only directories
|
|
10
|
+
* that NEVER carry user-authored source. `node_modules` is the JS-tooling cache
|
|
11
|
+
* (large; never AL); `.alpackages` is read separately by the app-package
|
|
12
|
+
* reader as binary `.app` files (walking it as a directory would surface those
|
|
13
|
+
* binary payloads, which the `.al` extension filter ignores anyway, but
|
|
14
|
+
* skipping avoids the no-op walk on potentially thousands of entries).
|
|
15
|
+
*
|
|
16
|
+
* Everything else — including dot-prefixed directories the user puts in their
|
|
17
|
+
* workspace — is walked. A folder is just a folder; its name carries no
|
|
18
|
+
* semantic meaning for analysis.
|
|
19
|
+
*/
|
|
20
|
+
const SKIP_DIR_EXACT: ReadonlySet<string> = new Set(["node_modules", ".alpackages"]);
|
|
21
|
+
|
|
22
|
+
/** Recursively list files under a directory. */
|
|
23
|
+
async function walk(dir: string): Promise<string[]> {
|
|
24
|
+
const out: string[] = [];
|
|
25
|
+
let entries: { name: string; isDirectory(): boolean }[];
|
|
26
|
+
try {
|
|
27
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
28
|
+
} catch {
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const full = join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
if (SKIP_DIR_EXACT.has(entry.name)) continue;
|
|
35
|
+
out.push(...(await walk(full)));
|
|
36
|
+
} else {
|
|
37
|
+
out.push(full);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Enumerates .al files in a workspace directory and reads its app.json identity. */
|
|
44
|
+
export class WorkspaceProvider implements SourceProvider {
|
|
45
|
+
readonly name = "workspace" as const;
|
|
46
|
+
|
|
47
|
+
async collect(rootPath: string): Promise<ProviderResult> {
|
|
48
|
+
const diagnostics: ProviderDiagnostic[] = [];
|
|
49
|
+
|
|
50
|
+
// --- app.json identity ---
|
|
51
|
+
let appGuid = "unknown";
|
|
52
|
+
let appName = "unknown";
|
|
53
|
+
let appPublisher = "unknown";
|
|
54
|
+
let appVersion = "0.0.0.0";
|
|
55
|
+
try {
|
|
56
|
+
const appJsonRaw = readFileSync(join(rootPath, "app.json"), "utf8");
|
|
57
|
+
const appJson = JSON.parse(appJsonRaw) as {
|
|
58
|
+
id?: string;
|
|
59
|
+
name?: string;
|
|
60
|
+
publisher?: string;
|
|
61
|
+
version?: string;
|
|
62
|
+
};
|
|
63
|
+
appGuid = appJson.id ?? appGuid;
|
|
64
|
+
appName = appJson.name ?? appName;
|
|
65
|
+
appPublisher = appJson.publisher ?? appPublisher;
|
|
66
|
+
appVersion = appJson.version ?? appVersion;
|
|
67
|
+
} catch {
|
|
68
|
+
diagnostics.push({
|
|
69
|
+
severity: "warning",
|
|
70
|
+
message: `No readable app.json at ${rootPath} — workspace app identity unknown`,
|
|
71
|
+
sourceRef: rootPath,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- enumerate .al files ---
|
|
76
|
+
const allFiles = await walk(rootPath);
|
|
77
|
+
const units: SourceUnit[] = [];
|
|
78
|
+
const contents: string[] = [];
|
|
79
|
+
for (const absPath of allFiles) {
|
|
80
|
+
if (!absPath.toLowerCase().endsWith(".al")) continue;
|
|
81
|
+
let content: string;
|
|
82
|
+
try {
|
|
83
|
+
content = readFileSync(absPath, "utf8");
|
|
84
|
+
} catch (err) {
|
|
85
|
+
diagnostics.push({
|
|
86
|
+
severity: "warning",
|
|
87
|
+
message: `Could not read ${absPath}: ${(err as Error).message}`,
|
|
88
|
+
sourceRef: absPath,
|
|
89
|
+
});
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const relativePath = relative(rootPath, absPath).split(sep).join("/");
|
|
93
|
+
units.push({
|
|
94
|
+
id: `ws:${relativePath}`,
|
|
95
|
+
kind: "source",
|
|
96
|
+
appGuid,
|
|
97
|
+
relativePath,
|
|
98
|
+
absolutePath: absPath,
|
|
99
|
+
content,
|
|
100
|
+
sourceProvider: "workspace",
|
|
101
|
+
analysisRole: "primary",
|
|
102
|
+
});
|
|
103
|
+
contents.push(content);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const identity: AppIdentity = {
|
|
107
|
+
appGuid,
|
|
108
|
+
publisher: appPublisher,
|
|
109
|
+
name: appName,
|
|
110
|
+
version: appVersion,
|
|
111
|
+
sourceAggregateHash: sha256OfStrings(contents.sort()),
|
|
112
|
+
sourceKind: "workspace",
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return { units, apps: [identity], diagnostics };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ObjectRunKind } from "../model/callee.ts";
|
|
2
|
+
import type { CallSite, Routine } from "../model/entities.ts";
|
|
3
|
+
import { type CallEdge, type DispatchKind, TREE_SITTER_EVIDENCE } from "../model/graph.ts";
|
|
4
|
+
import type { SemanticIndex } from "../model/model.ts";
|
|
5
|
+
import type { SymbolTable } from "./symbol-table.ts";
|
|
6
|
+
|
|
7
|
+
/** Map an object-run objectKind to its CallEdge dispatch kind. */
|
|
8
|
+
function objectRunDispatchKind(objectKind: ObjectRunKind): DispatchKind {
|
|
9
|
+
if (objectKind === "Page") return "page-run";
|
|
10
|
+
if (objectKind === "Report") return "report-run";
|
|
11
|
+
return "codeunit-run";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Upgrade a callsite's argumentBindings with callee-side var-ness once the callee
|
|
16
|
+
* is known. This upgrades `calleeParameterIsVar` and sets `bindingResolution` to
|
|
17
|
+
* "resolved" for any binding that was previously "unresolved-callee".
|
|
18
|
+
* Bindings with `bindingResolution === "non-record-arg"` are left untouched.
|
|
19
|
+
*
|
|
20
|
+
* See CallSite.argumentBindings JSDoc in entities.ts for the mutation contract:
|
|
21
|
+
* this function is the sole permitted writer to existing bindings, and
|
|
22
|
+
* re-entrant resolution is unsupported. The assertion below makes any future
|
|
23
|
+
* double-upgrade fail loudly instead of silently corrupting bindings.
|
|
24
|
+
*/
|
|
25
|
+
function upgradeBindings(callSite: CallSite, calleeRoutine: Routine): void {
|
|
26
|
+
// Sanity: re-running upgradeBindings on the same callsite is unsound (see
|
|
27
|
+
// entities.ts CallSite.argumentBindings mutation contract). Cheap dev-time check:
|
|
28
|
+
for (const binding of callSite.argumentBindings) {
|
|
29
|
+
if (binding.bindingResolution === "resolved" || binding.bindingResolution === "ambiguous") {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`call-resolver: argumentBindings for callsite ${callSite.id} already upgraded; re-entrant resolution is not supported`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const calleeParams = calleeRoutine.parameters;
|
|
36
|
+
for (let i = 0; i < callSite.argumentBindings.length; i++) {
|
|
37
|
+
const binding = callSite.argumentBindings[i];
|
|
38
|
+
if (binding === undefined) continue;
|
|
39
|
+
if (binding.bindingResolution === "non-record-arg") continue;
|
|
40
|
+
const calleeParam = calleeParams[i];
|
|
41
|
+
if (calleeParam === undefined) continue; // arity mismatch — leave defaults
|
|
42
|
+
binding.calleeParameterIsVar = calleeParam.isVar;
|
|
43
|
+
binding.bindingResolution = "resolved";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Resolve one call site within `routine` into a CallEdge. */
|
|
48
|
+
function resolveCallSite(routine: Routine, callSite: CallSite, symbols: SymbolTable): CallEdge {
|
|
49
|
+
const base = {
|
|
50
|
+
from: routine.id,
|
|
51
|
+
callsiteId: callSite.id,
|
|
52
|
+
operationId: callSite.operationId,
|
|
53
|
+
provenance: [TREE_SITTER_EVIDENCE],
|
|
54
|
+
};
|
|
55
|
+
const callee = callSite.callee;
|
|
56
|
+
|
|
57
|
+
switch (callee.kind) {
|
|
58
|
+
case "bare": {
|
|
59
|
+
// A bare call resolves to a procedure in the SAME object (AL has no free functions).
|
|
60
|
+
const target = symbols.routineInObject(routine.objectId, callee.name);
|
|
61
|
+
if (target) {
|
|
62
|
+
// Phase 2: upgrade argumentBindings now that we know the callee signature.
|
|
63
|
+
upgradeBindings(callSite, target);
|
|
64
|
+
return { ...base, to: target.id, dispatchKind: "direct", resolution: "resolved" };
|
|
65
|
+
}
|
|
66
|
+
return { ...base, dispatchKind: "unresolved", resolution: "unknown" };
|
|
67
|
+
}
|
|
68
|
+
case "object-run": {
|
|
69
|
+
const dispatchKind = objectRunDispatchKind(callee.objectKind);
|
|
70
|
+
if (callee.targetRef === undefined) {
|
|
71
|
+
// Dynamic target (a variable) — known shape, unknown target.
|
|
72
|
+
return { ...base, dispatchKind: "dynamic", resolution: "unknown" };
|
|
73
|
+
}
|
|
74
|
+
const targetObject = callee.targetIsName
|
|
75
|
+
? symbols.objectByTypeName(callee.targetType, callee.targetRef)
|
|
76
|
+
: symbols.objectByTypeNumber(callee.targetType, Number.parseInt(callee.targetRef, 10));
|
|
77
|
+
if (!targetObject) {
|
|
78
|
+
// Target named/numbered but not in indexed source (e.g. symbol-only dependency).
|
|
79
|
+
return { ...base, dispatchKind, resolution: "opaque" };
|
|
80
|
+
}
|
|
81
|
+
// Resolve to the object's entry routine: OnRun trigger for codeunits, else the
|
|
82
|
+
// first routine. If none, the edge is resolved-to-object but routine-less.
|
|
83
|
+
const entry =
|
|
84
|
+
symbols.routineInObject(targetObject.id, "OnRun") ??
|
|
85
|
+
symbols.routinesInObject(targetObject.id)[0];
|
|
86
|
+
if (entry) {
|
|
87
|
+
// Phase 2: upgrade argumentBindings now that we know the callee signature.
|
|
88
|
+
upgradeBindings(callSite, entry);
|
|
89
|
+
return { ...base, to: entry.id, dispatchKind, resolution: "resolved" };
|
|
90
|
+
}
|
|
91
|
+
return { ...base, dispatchKind, resolution: "opaque" };
|
|
92
|
+
}
|
|
93
|
+
case "member": {
|
|
94
|
+
// A method call on an instance variable. Phase 1 does not type-track non-record
|
|
95
|
+
// variables, so the receiver type is unknown -> unresolved method dispatch.
|
|
96
|
+
return { ...base, dispatchKind: "method", resolution: "unknown" };
|
|
97
|
+
}
|
|
98
|
+
default: {
|
|
99
|
+
return { ...base, dispatchKind: "unresolved", resolution: "unknown" };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve every call site in the index into a CallEdge. Exactly one edge per call site.
|
|
106
|
+
* Unresolved calls are DATA (a CallEdge with no `to` and a non-"resolved" resolution),
|
|
107
|
+
* never a silent gap.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveCalls(index: SemanticIndex, symbols: SymbolTable): CallEdge[] {
|
|
110
|
+
const edges: CallEdge[] = [];
|
|
111
|
+
for (const routine of index.routines) {
|
|
112
|
+
for (const callSite of routine.features.callSites) {
|
|
113
|
+
edges.push(resolveCallSite(routine, callSite, symbols));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return edges;
|
|
117
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Diagnostic } from "../model/finding.ts";
|
|
2
|
+
import type { CallEdge } from "../model/graph.ts";
|
|
3
|
+
import type { AnalysisCoverage, SemanticIndex } from "../model/model.ts";
|
|
4
|
+
import type { SourceUnit } from "../providers/types.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build the AnalysisCoverage record — the first-class "no silent clean" accounting.
|
|
8
|
+
* `sourceUnitsTotal` counts only `kind: "source"` units. `sourceUnitsParsed` subtracts
|
|
9
|
+
* units that produced a `warning`-severity index diagnostic (the throw path — a unit that
|
|
10
|
+
* parsed but had no object declaration produced an `info` diagnostic and still counts as
|
|
11
|
+
* parsed).
|
|
12
|
+
*
|
|
13
|
+
* `unresolvedCallsites` = every call edge whose target could not be determined
|
|
14
|
+
* (`resolution === "unknown"`). This covers `dispatchKind: "unresolved"`, `"method"` on
|
|
15
|
+
* untyped receivers, `"dynamic"` object-run calls, etc.
|
|
16
|
+
* `dynamicDispatchSites` = the dynamic-dispatch subset (`dispatchKind === "dynamic"`).
|
|
17
|
+
* Overlap between the two is intentional — dynamic sites are a named sub-category of
|
|
18
|
+
* unresolved.
|
|
19
|
+
*/
|
|
20
|
+
export function buildCoverage(
|
|
21
|
+
index: SemanticIndex,
|
|
22
|
+
callGraph: CallEdge[],
|
|
23
|
+
units: SourceUnit[],
|
|
24
|
+
indexDiagnostics: Diagnostic[],
|
|
25
|
+
): AnalysisCoverage {
|
|
26
|
+
const sourceUnits = units.filter((u) => u.kind === "source");
|
|
27
|
+
const failedUnitRefs = new Set(
|
|
28
|
+
indexDiagnostics
|
|
29
|
+
.filter(
|
|
30
|
+
(d): d is Diagnostic & { sourceRef: string } =>
|
|
31
|
+
d.stage === "index" && d.severity === "warning" && d.sourceRef !== undefined,
|
|
32
|
+
)
|
|
33
|
+
.map((d) => d.sourceRef),
|
|
34
|
+
);
|
|
35
|
+
const sourceUnitsParsed = sourceUnits.filter((u) => !failedUnitRefs.has(u.id)).length;
|
|
36
|
+
|
|
37
|
+
const opaqueApps = index.identity.apps
|
|
38
|
+
.filter((a) => a.sourceKind === "symbol-only")
|
|
39
|
+
.map((a) => a.appGuid);
|
|
40
|
+
|
|
41
|
+
const routinesBodyAvailable = index.routines.filter((r) => r.bodyAvailable).length;
|
|
42
|
+
const routinesParseIncomplete = index.routines.filter((r) => r.parseIncomplete).map((r) => r.id);
|
|
43
|
+
|
|
44
|
+
const unresolvedCallsites = callGraph
|
|
45
|
+
.filter((e) => e.resolution === "unknown")
|
|
46
|
+
.map((e) => e.callsiteId);
|
|
47
|
+
const dynamicDispatchSites = callGraph
|
|
48
|
+
.filter((e) => e.dispatchKind === "dynamic")
|
|
49
|
+
.map((e) => e.operationId);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
sourceUnitsTotal: sourceUnits.length,
|
|
53
|
+
sourceUnitsParsed,
|
|
54
|
+
routinesTotal: index.routines.length,
|
|
55
|
+
routinesBodyAvailable,
|
|
56
|
+
routinesParseIncomplete,
|
|
57
|
+
opaqueApps,
|
|
58
|
+
unresolvedCallsites,
|
|
59
|
+
dynamicDispatchSites,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { sha256Hex } from "../hash.ts";
|
|
2
|
+
import { type AttributeInfo, findAttribute, qualifiedArg, stringArg } from "../model/attributes.ts";
|
|
3
|
+
import type { Routine } from "../model/entities.ts";
|
|
4
|
+
import {
|
|
5
|
+
type EventEdge,
|
|
6
|
+
type EventGraph,
|
|
7
|
+
type EventSymbol,
|
|
8
|
+
TREE_SITTER_EVIDENCE,
|
|
9
|
+
} from "../model/graph.ts";
|
|
10
|
+
import { encodeEventId, encodeObjectId } from "../model/ids.ts";
|
|
11
|
+
import type { SemanticIndex } from "../model/model.ts";
|
|
12
|
+
import type { SymbolTable } from "./symbol-table.ts";
|
|
13
|
+
|
|
14
|
+
/** Determine the event kind from a publisher routine's structured attributes. */
|
|
15
|
+
function publisherEventKind(attrs: readonly AttributeInfo[]): EventSymbol["eventKind"] {
|
|
16
|
+
if (findAttribute(attrs, "IntegrationEvent") !== undefined) return "integration";
|
|
17
|
+
if (findAttribute(attrs, "BusinessEvent") !== undefined) return "business";
|
|
18
|
+
return "unknown";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Read an `[EventSubscriber(ObjectType::X, X::"Y", 'EventName', 'ElementName', ...)]`
|
|
23
|
+
* attribute's target parts from the structured `AttributeInfo`, or null if no
|
|
24
|
+
* EventSubscriber is present.
|
|
25
|
+
*
|
|
26
|
+
* Args (positional, per AL spec):
|
|
27
|
+
* 0: ObjectType::<kind> (qualified_enum_value — `value` = the kind)
|
|
28
|
+
* 1: <kind>::<ref> or <kind>::"ref" (database_reference — `member` = the ref)
|
|
29
|
+
* 2: '<eventName>' (string_literal)
|
|
30
|
+
* 3: '<elementName>' (string_literal, may be empty)
|
|
31
|
+
*
|
|
32
|
+
* The grammar already distinguishes quoted vs unquoted target refs via
|
|
33
|
+
* `quoted_identifier` vs `identifier` inside `database_reference`, so the
|
|
34
|
+
* `.member` value is the unquoted ref regardless of source quoting.
|
|
35
|
+
*/
|
|
36
|
+
function parseSubscriberAttribute(attrs: readonly AttributeInfo[]): {
|
|
37
|
+
targetObjectType: string;
|
|
38
|
+
targetRef: string;
|
|
39
|
+
eventName: string;
|
|
40
|
+
elementName: string;
|
|
41
|
+
} | null {
|
|
42
|
+
const attr = findAttribute(attrs, "EventSubscriber");
|
|
43
|
+
if (attr === undefined) return null;
|
|
44
|
+
const objectTypeArg = qualifiedArg(attr, 0);
|
|
45
|
+
const targetRefArg = qualifiedArg(attr, 1);
|
|
46
|
+
const eventName = stringArg(attr, 2);
|
|
47
|
+
const elementName = stringArg(attr, 3);
|
|
48
|
+
if (objectTypeArg === undefined || targetRefArg === undefined || eventName === undefined) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
targetObjectType: objectTypeArg.member,
|
|
53
|
+
targetRef: targetRefArg.member,
|
|
54
|
+
eventName,
|
|
55
|
+
elementName: elementName ?? "",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Build the EventSymbol for a publisher routine. */
|
|
60
|
+
function buildEventSymbol(routine: Routine, publisherObjectId: string): EventSymbol {
|
|
61
|
+
return {
|
|
62
|
+
id: encodeEventId(publisherObjectId, routine.name),
|
|
63
|
+
publisherObjectId,
|
|
64
|
+
publisherRoutineId: routine.id,
|
|
65
|
+
eventName: routine.name,
|
|
66
|
+
eventKind: publisherEventKind(routine.attributesParsed),
|
|
67
|
+
signatureHash: routine.canonical.normalizedSignatureHash,
|
|
68
|
+
parameters: routine.parameters,
|
|
69
|
+
provenance: [TREE_SITTER_EVIDENCE],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build the event graph: EventSymbols from publisher routines, EventEdges from subscriber
|
|
75
|
+
* routines. A subscriber targeting an event al-sem cannot see (e.g. a Base App event in a
|
|
76
|
+
* symbol-only dependency) still produces an edge — with a synthesized eventId and a
|
|
77
|
+
* non-"resolved" resolution — never a silent gap.
|
|
78
|
+
*/
|
|
79
|
+
export function buildEventGraph(index: SemanticIndex, symbols: SymbolTable): EventGraph {
|
|
80
|
+
const events: EventSymbol[] = [];
|
|
81
|
+
const eventById = new Map<string, EventSymbol>();
|
|
82
|
+
const objectById = new Map(index.objects.map((o) => [o.id, o]));
|
|
83
|
+
|
|
84
|
+
// --- publishers ---
|
|
85
|
+
for (const routine of index.routines) {
|
|
86
|
+
if (routine.kind !== "event-publisher") continue;
|
|
87
|
+
const symbol = buildEventSymbol(routine, routine.objectId);
|
|
88
|
+
events.push(symbol);
|
|
89
|
+
eventById.set(symbol.id, symbol);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- subscribers ---
|
|
93
|
+
const edges: EventEdge[] = [];
|
|
94
|
+
for (const routine of index.routines) {
|
|
95
|
+
if (routine.kind !== "event-subscriber") continue;
|
|
96
|
+
const target = parseSubscriberAttribute(routine.attributesParsed);
|
|
97
|
+
if (!target) continue;
|
|
98
|
+
|
|
99
|
+
const subscriberObject = objectById.get(routine.objectId);
|
|
100
|
+
// routine.objectId should always resolve to an indexed object; the "unknown"
|
|
101
|
+
// sentinel here signals an inconsistent index (a routine without its owning object).
|
|
102
|
+
const subscriberAppId = subscriberObject?.appGuid ?? "unknown";
|
|
103
|
+
|
|
104
|
+
// Resolve the target object by type + name (AL names are case-insensitive).
|
|
105
|
+
const targetObject = symbols.objectByTypeName(target.targetObjectType, target.targetRef);
|
|
106
|
+
|
|
107
|
+
let eventId: string;
|
|
108
|
+
let resolution: EventEdge["resolution"];
|
|
109
|
+
|
|
110
|
+
if (targetObject) {
|
|
111
|
+
eventId = encodeEventId(targetObject.id, target.eventName);
|
|
112
|
+
// "resolved" only if we also found the matching publisher symbol.
|
|
113
|
+
if (eventById.has(eventId)) {
|
|
114
|
+
resolution = "resolved";
|
|
115
|
+
} else {
|
|
116
|
+
resolution = "maybe";
|
|
117
|
+
// Synthesize a symbol for a target object whose publisher was not indexed.
|
|
118
|
+
// (We are already in the else-branch of `eventById.has(eventId)`, so the
|
|
119
|
+
// symbol is guaranteed absent — no redundant guard needed here.)
|
|
120
|
+
const synthesized: EventSymbol = {
|
|
121
|
+
id: eventId,
|
|
122
|
+
publisherObjectId: targetObject.id,
|
|
123
|
+
eventName: target.eventName,
|
|
124
|
+
eventKind: "unknown",
|
|
125
|
+
elementName: target.elementName !== "" ? target.elementName : undefined,
|
|
126
|
+
signatureHash: sha256Hex(eventId),
|
|
127
|
+
parameters: [],
|
|
128
|
+
provenance: [{ source: "tree-sitter", note: "publisher not indexed" }],
|
|
129
|
+
};
|
|
130
|
+
events.push(synthesized);
|
|
131
|
+
eventById.set(eventId, synthesized);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
// Target object not in indexed source — synthesize a pseudo object id.
|
|
135
|
+
// NOTE: `${pseudoObjectId}:${targetRef}` is a non-conforming sentinel string,
|
|
136
|
+
// NOT a valid ObjectId — downstream consumers must not attempt to parse it.
|
|
137
|
+
const pseudoObjectId = encodeObjectId("unknown", target.targetObjectType, 0);
|
|
138
|
+
eventId = encodeEventId(`${pseudoObjectId}:${target.targetRef}`, target.eventName);
|
|
139
|
+
resolution = "unknown";
|
|
140
|
+
if (!eventById.has(eventId)) {
|
|
141
|
+
const synthesized: EventSymbol = {
|
|
142
|
+
id: eventId,
|
|
143
|
+
publisherObjectId: `${pseudoObjectId}:${target.targetRef}`,
|
|
144
|
+
eventName: target.eventName,
|
|
145
|
+
eventKind: "unknown",
|
|
146
|
+
elementName: target.elementName !== "" ? target.elementName : undefined,
|
|
147
|
+
signatureHash: sha256Hex(eventId),
|
|
148
|
+
parameters: [],
|
|
149
|
+
provenance: [{ source: "tree-sitter", note: "target object not in indexed source" }],
|
|
150
|
+
};
|
|
151
|
+
events.push(synthesized);
|
|
152
|
+
eventById.set(eventId, synthesized);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
edges.push({
|
|
157
|
+
eventId,
|
|
158
|
+
subscriberRoutineId: routine.id,
|
|
159
|
+
subscriberAppId,
|
|
160
|
+
resolution,
|
|
161
|
+
provenance: [TREE_SITTER_EVIDENCE],
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { events, edges };
|
|
166
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { RecordOpType } from "../model/entities.ts";
|
|
2
|
+
import { type CallEdge, type ResolutionQuality, TREE_SITTER_EVIDENCE } from "../model/graph.ts";
|
|
3
|
+
import type { SemanticIndex } from "../model/model.ts";
|
|
4
|
+
import type { SymbolTable } from "./symbol-table.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Maps a trigger-invoking record op to (the table trigger name it invokes, the resolution
|
|
8
|
+
* quality of the edge). `Validate` always runs the field's OnValidate, so "resolved".
|
|
9
|
+
* `Insert`/`Modify`/`Delete` run the table trigger only when called with
|
|
10
|
+
* `RunTrigger = true`, which Phase 1 does not capture — so "maybe".
|
|
11
|
+
*/
|
|
12
|
+
const TRIGGER_OPS: Partial<
|
|
13
|
+
Record<RecordOpType, { triggerName: string; resolution: ResolutionQuality }>
|
|
14
|
+
> = {
|
|
15
|
+
Validate: { triggerName: "OnValidate", resolution: "resolved" },
|
|
16
|
+
Insert: { triggerName: "OnInsert", resolution: "maybe" },
|
|
17
|
+
Modify: { triggerName: "OnModify", resolution: "maybe" },
|
|
18
|
+
Delete: { triggerName: "OnDelete", resolution: "maybe" },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build implicit-trigger CallEdges. For each trigger-invoking record op whose record
|
|
23
|
+
* variable resolves to a table that IS in indexed source, emit an edge to that table's
|
|
24
|
+
* trigger routine. Tables al-sem cannot see (symbol-only dependencies, unknown tables)
|
|
25
|
+
* produce no edge — that absence is reflected in AnalysisCoverage, not invented here.
|
|
26
|
+
*/
|
|
27
|
+
export function buildImplicitTriggerEdges(index: SemanticIndex, symbols: SymbolTable): CallEdge[] {
|
|
28
|
+
const edges: CallEdge[] = [];
|
|
29
|
+
for (const routine of index.routines) {
|
|
30
|
+
for (const op of routine.features.recordOperations) {
|
|
31
|
+
const mapping = TRIGGER_OPS[op.op];
|
|
32
|
+
if (!mapping) continue;
|
|
33
|
+
if (!op.tableId) continue; // table not resolved -> cannot find its trigger
|
|
34
|
+
const table = symbols.tableById(op.tableId);
|
|
35
|
+
if (!table) continue;
|
|
36
|
+
// The table's object id: tables are objects too. Look it up by type+number.
|
|
37
|
+
const tableObject = symbols.objectByTypeNumber("Table", table.tableNumber);
|
|
38
|
+
if (!tableObject) continue;
|
|
39
|
+
const trigger = symbols.routineInObject(tableObject.id, mapping.triggerName);
|
|
40
|
+
if (!trigger) continue;
|
|
41
|
+
edges.push({
|
|
42
|
+
from: routine.id,
|
|
43
|
+
to: trigger.id,
|
|
44
|
+
callsiteId: op.id, // the record-op's operation id doubles as the callsite ref
|
|
45
|
+
operationId: op.id,
|
|
46
|
+
dispatchKind: "implicit-trigger",
|
|
47
|
+
resolution: mapping.resolution,
|
|
48
|
+
provenance: [TREE_SITTER_EVIDENCE],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return edges;
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { SemanticIndex } from "../model/model.ts";
|
|
2
|
+
import type { SymbolTable } from "./symbol-table.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Back-fill `tableId` on record variables and record operations, and `recordVariableId`
|
|
6
|
+
* on record operations, by resolving table names against the SymbolTable. Mutates the
|
|
7
|
+
* index's routines in place (the established pattern — Phase 1's routine indexer also
|
|
8
|
+
* mutates `callSite.loopStack` in place). A record variable naming a table al-sem cannot
|
|
9
|
+
* see (e.g. a base-app table in a symbol-only dependency) is left with `tableId`
|
|
10
|
+
* undefined — never guessed.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveRecordTypes(index: SemanticIndex, symbols: SymbolTable): void {
|
|
13
|
+
for (const routine of index.routines) {
|
|
14
|
+
const { recordVariables, recordOperations } = routine.features;
|
|
15
|
+
|
|
16
|
+
// --- resolve record variables ---
|
|
17
|
+
// name (lowercased) -> the resolved variable, for matching operations below.
|
|
18
|
+
const varByName = new Map<string, (typeof recordVariables)[number]>();
|
|
19
|
+
for (const variable of recordVariables) {
|
|
20
|
+
varByName.set(variable.name.toLowerCase(), variable);
|
|
21
|
+
if (variable.tableName) {
|
|
22
|
+
const table = symbols.tableByName(variable.tableName);
|
|
23
|
+
if (table) variable.tableId = table.id;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// --- resolve record operations against their record variable ---
|
|
28
|
+
for (const op of recordOperations) {
|
|
29
|
+
const variable = varByName.get(op.recordVariableName.toLowerCase());
|
|
30
|
+
if (variable) {
|
|
31
|
+
op.recordVariableId = variable.id;
|
|
32
|
+
if (variable.tableId) op.tableId = variable.tableId;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Diagnostic } from "../model/finding.ts";
|
|
2
|
+
import type { SemanticIndex, SemanticModel } from "../model/model.ts";
|
|
3
|
+
import type { SourceUnit } from "../providers/types.ts";
|
|
4
|
+
import { buildCoverage } from "./coverage.ts";
|
|
5
|
+
import { resolveSemanticGraph } from "./semantic-graph.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* L3 orchestrator for the WORKSPACE pipeline: extend a SemanticIndex into a SemanticModel by
|
|
9
|
+
* resolving its semantic graphs (`resolveSemanticGraph`) and adding the workspace coverage
|
|
10
|
+
* record. `units` and `indexDiagnostics` feed only `buildCoverage`.
|
|
11
|
+
*
|
|
12
|
+
* Pass `[]` for both `units` and `indexDiagnostics` in unit tests that work from a
|
|
13
|
+
* pre-built index.
|
|
14
|
+
*/
|
|
15
|
+
export function resolveModel(
|
|
16
|
+
index: SemanticIndex,
|
|
17
|
+
units: SourceUnit[],
|
|
18
|
+
indexDiagnostics: Diagnostic[],
|
|
19
|
+
): SemanticModel {
|
|
20
|
+
const { callGraph, eventGraph } = resolveSemanticGraph(index);
|
|
21
|
+
const coverage = buildCoverage(index, callGraph, units, indexDiagnostics);
|
|
22
|
+
return { ...index, callGraph, eventGraph, coverage, rootClassifications: [] };
|
|
23
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CallEdge, EventGraph } from "../model/graph.ts";
|
|
2
|
+
import type { SemanticIndex } from "../model/model.ts";
|
|
3
|
+
import { resolveCalls } from "./call-resolver.ts";
|
|
4
|
+
import { buildEventGraph } from "./event-graph.ts";
|
|
5
|
+
import { buildImplicitTriggerEdges } from "./implicit-edges.ts";
|
|
6
|
+
import { resolveRecordTypes } from "./record-types.ts";
|
|
7
|
+
import { type SymbolTable, buildSymbolTable } from "./symbol-table.ts";
|
|
8
|
+
|
|
9
|
+
export interface SemanticGraphResult {
|
|
10
|
+
symbols: SymbolTable;
|
|
11
|
+
callGraph: CallEdge[];
|
|
12
|
+
eventGraph: EventGraph;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The shared graph-resolution core: build the symbol table, resolve record-variable table
|
|
17
|
+
* types in place, then build the call graph (call edges + implicit-trigger edges) and event
|
|
18
|
+
* graph. Used by both the workspace pipeline (`resolveModel`) and the dependency-mode
|
|
19
|
+
* pipeline. Does NOT build coverage — that is workspace-specific and stays in `resolveModel`.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveSemanticGraph(index: SemanticIndex): SemanticGraphResult {
|
|
22
|
+
const symbols = buildSymbolTable(index);
|
|
23
|
+
resolveRecordTypes(index, symbols);
|
|
24
|
+
const callEdges = resolveCalls(index, symbols);
|
|
25
|
+
const implicitEdges = buildImplicitTriggerEdges(index, symbols);
|
|
26
|
+
const callGraph = [...callEdges, ...implicitEdges];
|
|
27
|
+
const eventGraph = buildEventGraph(index, symbols);
|
|
28
|
+
return { symbols, callGraph, eventGraph };
|
|
29
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ObjectDecl, Routine, Table } from "../model/entities.ts";
|
|
2
|
+
import type { ObjectId } from "../model/ids.ts";
|
|
3
|
+
import type { SemanticIndex } from "../model/model.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A read-only lookup index over a SemanticIndex. Built once, queried by every resolver.
|
|
7
|
+
* All name lookups are case-insensitive (AL identifiers are case-insensitive).
|
|
8
|
+
*/
|
|
9
|
+
export interface SymbolTable {
|
|
10
|
+
objectByTypeNumber(objectType: string, objectNumber: number): ObjectDecl | undefined;
|
|
11
|
+
objectByTypeName(objectType: string, name: string): ObjectDecl | undefined;
|
|
12
|
+
tableByName(name: string): Table | undefined;
|
|
13
|
+
tableById(id: string): Table | undefined;
|
|
14
|
+
routineInObject(objectId: ObjectId, routineName: string): Routine | undefined;
|
|
15
|
+
routinesInObject(objectId: ObjectId): Routine[];
|
|
16
|
+
routineById(routineId: string): Routine | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildSymbolTable(index: SemanticIndex): SymbolTable {
|
|
20
|
+
const byTypeNumber = new Map<string, ObjectDecl>();
|
|
21
|
+
const byTypeName = new Map<string, ObjectDecl>();
|
|
22
|
+
for (const o of index.objects) {
|
|
23
|
+
byTypeNumber.set(`${o.objectType.toLowerCase()}/${o.objectNumber}`, o);
|
|
24
|
+
byTypeName.set(`${o.objectType.toLowerCase()}/${o.name.toLowerCase()}`, o);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const tablesByName = new Map<string, Table>();
|
|
28
|
+
const tablesById = new Map<string, Table>();
|
|
29
|
+
for (const t of index.tables) {
|
|
30
|
+
tablesByName.set(t.name.toLowerCase(), t);
|
|
31
|
+
tablesById.set(t.id, t);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Routines keyed by `${objectId}::${routineName.toLowerCase()}`, and grouped per object.
|
|
35
|
+
const routineByKey = new Map<string, Routine>();
|
|
36
|
+
const routinesByObject = new Map<string, Routine[]>();
|
|
37
|
+
const routinesById = new Map<string, Routine>();
|
|
38
|
+
for (const r of index.routines) {
|
|
39
|
+
routineByKey.set(`${r.objectId}::${r.name.toLowerCase()}`, r);
|
|
40
|
+
routinesById.set(r.id, r);
|
|
41
|
+
const list = routinesByObject.get(r.objectId);
|
|
42
|
+
if (list) list.push(r);
|
|
43
|
+
else routinesByObject.set(r.objectId, [r]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
objectByTypeNumber(objectType, objectNumber) {
|
|
48
|
+
return byTypeNumber.get(`${objectType.toLowerCase()}/${objectNumber}`);
|
|
49
|
+
},
|
|
50
|
+
objectByTypeName(objectType, name) {
|
|
51
|
+
return byTypeName.get(`${objectType.toLowerCase()}/${name.toLowerCase()}`);
|
|
52
|
+
},
|
|
53
|
+
tableByName(name) {
|
|
54
|
+
return tablesByName.get(name.toLowerCase());
|
|
55
|
+
},
|
|
56
|
+
tableById(id) {
|
|
57
|
+
return tablesById.get(id);
|
|
58
|
+
},
|
|
59
|
+
routineInObject(objectId, routineName) {
|
|
60
|
+
return routineByKey.get(`${objectId}::${routineName.toLowerCase()}`);
|
|
61
|
+
},
|
|
62
|
+
routinesInObject(objectId) {
|
|
63
|
+
return routinesByObject.get(objectId) ?? [];
|
|
64
|
+
},
|
|
65
|
+
routineById(routineId) {
|
|
66
|
+
return routinesById.get(routineId);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|