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,279 @@
|
|
|
1
|
+
import type { Field, Key, ObjectDecl, Table } from "../model/entities.ts";
|
|
2
|
+
import { encodeFieldId, encodeKeyId, encodeObjectId, encodeTableId } from "../model/ids.ts";
|
|
3
|
+
import { isPropertyNamed, nodeToSourceRange, stripQuotes } from "../parser/ast.ts";
|
|
4
|
+
import type { Node as SyntaxNode, Tree } from "../parser/native/wrapper.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Read a property value by name from an object declaration. AL property syntax:
|
|
8
|
+
* `Subtype = Install;`
|
|
9
|
+
* exposed by the grammar as a `property` named-child of the object declaration.
|
|
10
|
+
* The grammar exposes the property key via `childForFieldName("name")` and the
|
|
11
|
+
* value via `childForFieldName("value")`. The child nodes are typed `property_name`
|
|
12
|
+
* and `identifier` respectively — `property_name` is a node TYPE, not a field name.
|
|
13
|
+
* Returns the raw value text (preserves casing) or undefined.
|
|
14
|
+
*/
|
|
15
|
+
function readObjectProperty(decl: SyntaxNode, propertyName: string): string | undefined {
|
|
16
|
+
for (let i = 0; i < decl.namedChildCount; i++) {
|
|
17
|
+
const child = decl.namedChild(i);
|
|
18
|
+
if (child === null || child.type !== "property") continue;
|
|
19
|
+
const nameNode = child.childForFieldName("name");
|
|
20
|
+
if (nameNode === null) continue;
|
|
21
|
+
if (nameNode.text.toLowerCase() !== propertyName.toLowerCase()) continue;
|
|
22
|
+
const valueNode = child.childForFieldName("value");
|
|
23
|
+
if (valueNode !== null) return valueNode.text;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Maps V2 grammar object declaration node types to display object-type names. */
|
|
30
|
+
const OBJECT_TYPE_MAP: Record<string, string> = {
|
|
31
|
+
codeunit_declaration: "Codeunit",
|
|
32
|
+
table_declaration: "Table",
|
|
33
|
+
tableextension_declaration: "TableExtension",
|
|
34
|
+
page_declaration: "Page",
|
|
35
|
+
pageextension_declaration: "PageExtension",
|
|
36
|
+
report_declaration: "Report",
|
|
37
|
+
reportextension_declaration: "ReportExtension",
|
|
38
|
+
query_declaration: "Query",
|
|
39
|
+
xmlport_declaration: "XMLport",
|
|
40
|
+
enum_declaration: "Enum",
|
|
41
|
+
enumextension_declaration: "EnumExtension",
|
|
42
|
+
interface_declaration: "Interface",
|
|
43
|
+
controladdin_declaration: "ControlAddIn",
|
|
44
|
+
permissionset_declaration: "PermissionSet",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface IndexObjectInput {
|
|
48
|
+
tree: Tree;
|
|
49
|
+
appGuid: string;
|
|
50
|
+
sourceUnitId: string;
|
|
51
|
+
/** Unused by the object indexer; present so callers share one input type with the routine indexer, which consumes it. */
|
|
52
|
+
modelInstanceId: string;
|
|
53
|
+
sourceHash: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface IndexObjectResult {
|
|
57
|
+
object?: ObjectDecl;
|
|
58
|
+
/** The object declaration node, for the routine indexer to walk further. */
|
|
59
|
+
objectNode?: SyntaxNode;
|
|
60
|
+
objectType?: string;
|
|
61
|
+
/** Present for table and tableextension declarations. */
|
|
62
|
+
table?: Table;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function findObjectDeclarations(root: SyntaxNode): SyntaxNode[] {
|
|
66
|
+
const out: SyntaxNode[] = [];
|
|
67
|
+
for (const child of root.namedChildren) {
|
|
68
|
+
if (child && child.type in OBJECT_TYPE_MAP) out.push(child);
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractObjectNumber(decl: SyntaxNode): number {
|
|
74
|
+
for (const child of decl.namedChildren) {
|
|
75
|
+
if (child?.type === "integer") return Number.parseInt(child.text, 10);
|
|
76
|
+
}
|
|
77
|
+
// Object types without a number (e.g. interface) yield 0 — acceptable in Phase 1
|
|
78
|
+
// since such objects carry no tables/fields.
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractObjectName(decl: SyntaxNode): string {
|
|
83
|
+
for (const child of decl.namedChildren) {
|
|
84
|
+
if (child?.type === "quoted_identifier") return stripQuotes(child.text);
|
|
85
|
+
if (child?.type === "identifier") return child.text;
|
|
86
|
+
}
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const BLOB_LIKE = new Set(["blob", "media", "mediaset"]);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Classify a field_declaration node's FieldClass and Blob status.
|
|
94
|
+
* Uses `type_specification` for the data type (confirmed V2 grammar).
|
|
95
|
+
* Uses generic `property` nodes with isPropertyNamed("FieldClass") for class.
|
|
96
|
+
*/
|
|
97
|
+
function classifyField(fieldNode: SyntaxNode): {
|
|
98
|
+
dataType: string;
|
|
99
|
+
fieldClass: Field["fieldClass"];
|
|
100
|
+
isBlobLike: boolean;
|
|
101
|
+
} {
|
|
102
|
+
let dataType = "";
|
|
103
|
+
|
|
104
|
+
// type_specification is the confirmed V2 node for field data types
|
|
105
|
+
for (const child of fieldNode.namedChildren) {
|
|
106
|
+
if (child?.type === "type_specification") {
|
|
107
|
+
dataType = child.text;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let fieldClass: Field["fieldClass"] = "Normal";
|
|
113
|
+
|
|
114
|
+
// FieldClass is always a direct `property` child of field_declaration in the V2 grammar.
|
|
115
|
+
for (const child of fieldNode.namedChildren) {
|
|
116
|
+
if (!child) continue;
|
|
117
|
+
if (isPropertyNamed(child, "FieldClass")) {
|
|
118
|
+
const value = child.childForFieldName("value")?.text ?? "";
|
|
119
|
+
if (/flowfield/i.test(value)) fieldClass = "FlowField";
|
|
120
|
+
else if (/flowfilter/i.test(value)) fieldClass = "FlowFilter";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const isBlobLike = BLOB_LIKE.has(dataType.toLowerCase());
|
|
125
|
+
return { dataType, fieldClass, isBlobLike };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build a Table (with Fields and Keys) from a table/tableextension declaration node.
|
|
130
|
+
* Uses V2 grammar node types: `field_declaration` for fields, `key_declaration` for keys,
|
|
131
|
+
* `field_list` for the list of field names in a key.
|
|
132
|
+
*/
|
|
133
|
+
function indexTable(
|
|
134
|
+
decl: SyntaxNode,
|
|
135
|
+
objectId: string,
|
|
136
|
+
appGuid: string,
|
|
137
|
+
tableNumber: number,
|
|
138
|
+
tableName: string,
|
|
139
|
+
): Table {
|
|
140
|
+
const tableId = encodeTableId(appGuid, tableNumber);
|
|
141
|
+
const fields: Field[] = [];
|
|
142
|
+
const keys: Key[] = [];
|
|
143
|
+
|
|
144
|
+
// Collect `field_declaration` and `key_declaration` nodes anywhere under the declaration.
|
|
145
|
+
const fieldNodes: SyntaxNode[] = [];
|
|
146
|
+
const keyNodes: SyntaxNode[] = [];
|
|
147
|
+
const stack: SyntaxNode[] = [decl];
|
|
148
|
+
while (stack.length > 0) {
|
|
149
|
+
const node = stack.pop();
|
|
150
|
+
if (!node) continue;
|
|
151
|
+
if (node.type === "field_declaration") {
|
|
152
|
+
fieldNodes.push(node);
|
|
153
|
+
// Don't recurse into nested field declarations
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (node.type === "key_declaration") {
|
|
157
|
+
keyNodes.push(node);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
// Push in reverse so the stack pops in document order
|
|
161
|
+
for (let i = node.namedChildren.length - 1; i >= 0; i--) {
|
|
162
|
+
const child = node.namedChildren[i];
|
|
163
|
+
if (child) stack.push(child);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const fieldNode of fieldNodes) {
|
|
168
|
+
// field_declaration(<number>; <name>; <type>) — number is first integer,
|
|
169
|
+
// quoted name is quoted_identifier, unquoted name is identifier (after the integer).
|
|
170
|
+
let fieldNumber = 0;
|
|
171
|
+
let fieldName = "";
|
|
172
|
+
let nameFound = false;
|
|
173
|
+
for (const child of fieldNode.namedChildren) {
|
|
174
|
+
if (!child) continue;
|
|
175
|
+
if (fieldNumber === 0 && child.type === "integer") {
|
|
176
|
+
fieldNumber = Number.parseInt(child.text, 10);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (!nameFound && fieldNumber !== 0) {
|
|
180
|
+
if (child.type === "quoted_identifier") {
|
|
181
|
+
fieldName = stripQuotes(child.text);
|
|
182
|
+
nameFound = true;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (child.type === "identifier") {
|
|
186
|
+
fieldName = child.text;
|
|
187
|
+
nameFound = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const { dataType, fieldClass, isBlobLike } = classifyField(fieldNode);
|
|
192
|
+
fields.push({
|
|
193
|
+
id: encodeFieldId(tableId, fieldNumber),
|
|
194
|
+
physicalTableId: tableId,
|
|
195
|
+
declaringObjectId: objectId,
|
|
196
|
+
declaringAppId: appGuid,
|
|
197
|
+
fieldNumber,
|
|
198
|
+
name: fieldName,
|
|
199
|
+
fieldClass,
|
|
200
|
+
dataType,
|
|
201
|
+
isBlobLike,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const fieldsByName = new Map(fields.map((f) => [f.name.toLowerCase(), f]));
|
|
206
|
+
|
|
207
|
+
keyNodes.forEach((keyNode, index) => {
|
|
208
|
+
// key_declaration: name via childForFieldName("name"),
|
|
209
|
+
// fields via `field_list` child containing identifier/quoted_identifier nodes.
|
|
210
|
+
const keyFieldIds: string[] = [];
|
|
211
|
+
const keyFieldList = keyNode.namedChildren.find((c) => c !== null && c.type === "field_list");
|
|
212
|
+
if (keyFieldList) {
|
|
213
|
+
for (const child of keyFieldList.namedChildren) {
|
|
214
|
+
if (!child) continue;
|
|
215
|
+
const resolved = fieldsByName.get(stripQuotes(child.text).toLowerCase());
|
|
216
|
+
// a key field not found in this object is silently skipped — cross-object/tableextension field resolution happens in Phase 2
|
|
217
|
+
if (resolved) keyFieldIds.push(resolved.id);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
keys.push({
|
|
221
|
+
id: encodeKeyId(tableId, index),
|
|
222
|
+
physicalTableId: tableId,
|
|
223
|
+
declaringObjectId: objectId,
|
|
224
|
+
fields: keyFieldIds,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return { id: tableId, appGuid, tableNumber, name: tableName, fields, keys };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Index all top-level object declarations in a parsed source file. */
|
|
232
|
+
export function indexObjects(input: IndexObjectInput): IndexObjectResult[] {
|
|
233
|
+
const { tree, appGuid, sourceUnitId, sourceHash } = input;
|
|
234
|
+
const decls = findObjectDeclarations(tree.rootNode);
|
|
235
|
+
const results: IndexObjectResult[] = [];
|
|
236
|
+
|
|
237
|
+
for (const decl of decls) {
|
|
238
|
+
const objectType = OBJECT_TYPE_MAP[decl.type] ?? "Unknown";
|
|
239
|
+
const objectNumber = extractObjectNumber(decl);
|
|
240
|
+
const name = extractObjectName(decl);
|
|
241
|
+
const objectId = encodeObjectId(appGuid, objectType, objectNumber);
|
|
242
|
+
|
|
243
|
+
let objectSubtype: string | undefined;
|
|
244
|
+
let pageType: string | undefined;
|
|
245
|
+
if (objectType === "Codeunit") {
|
|
246
|
+
objectSubtype = readObjectProperty(decl, "Subtype");
|
|
247
|
+
}
|
|
248
|
+
if (objectType === "Page" || objectType === "PageExtension") {
|
|
249
|
+
pageType = readObjectProperty(decl, "PageType");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const object: ObjectDecl = {
|
|
253
|
+
id: objectId,
|
|
254
|
+
appGuid,
|
|
255
|
+
objectType,
|
|
256
|
+
objectNumber,
|
|
257
|
+
name,
|
|
258
|
+
sourceUnitId,
|
|
259
|
+
sourceHash,
|
|
260
|
+
sourceAnchor: {
|
|
261
|
+
sourceUnitId,
|
|
262
|
+
range: nodeToSourceRange(decl),
|
|
263
|
+
enclosingRoutineId: "",
|
|
264
|
+
syntaxKind: decl.type,
|
|
265
|
+
},
|
|
266
|
+
objectSubtype,
|
|
267
|
+
pageType,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
let table: Table | undefined;
|
|
271
|
+
if (objectType === "Table" || objectType === "TableExtension") {
|
|
272
|
+
table = indexTable(decl, objectId, appGuid, objectNumber, name);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
results.push({ object, objectNode: decl, objectType, table });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return results;
|
|
279
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { sha256Hex, sha256HexBytes } from "../hash.ts";
|
|
2
|
+
import type { AttributeInfo } from "../model/attributes.ts";
|
|
3
|
+
import type {
|
|
4
|
+
ConditionReference,
|
|
5
|
+
ControlFlowNode,
|
|
6
|
+
IntraproceduralFeatures,
|
|
7
|
+
LoopNode,
|
|
8
|
+
ObjectDecl,
|
|
9
|
+
ProcedureAccessModifier,
|
|
10
|
+
Routine,
|
|
11
|
+
VarAssignment,
|
|
12
|
+
} from "../model/entities.ts";
|
|
13
|
+
import { type CanonicalRoutineKey, type RoutineKind, encodeRoutineId } from "../model/ids.ts";
|
|
14
|
+
import { collectDescendants, nodeToSourceRange, stripQuotes } from "../parser/ast.ts";
|
|
15
|
+
import type { Node as SyntaxNode } from "../parser/native/wrapper.ts";
|
|
16
|
+
import { attributeInfoFromNode } from "./attribute-from-node.ts";
|
|
17
|
+
import { extractBodyFeatures } from "./intraprocedural-body.ts";
|
|
18
|
+
import { extractParameters, extractRecordVariablesAndParameters } from "./intraprocedural-refs.ts";
|
|
19
|
+
import { canonicalRoutineSignature } from "./routine-signature.ts";
|
|
20
|
+
import { extractVariables } from "./variable-indexer.ts";
|
|
21
|
+
|
|
22
|
+
export interface IndexRoutinesInput {
|
|
23
|
+
objectNode: SyntaxNode;
|
|
24
|
+
object: ObjectDecl;
|
|
25
|
+
sourceUnitId: string;
|
|
26
|
+
modelInstanceId: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Walk a routine's preceding `attribute_item` sibling nodes once, returning the
|
|
31
|
+
* routine's classification plus raw attribute texts and structured `AttributeInfo[]`
|
|
32
|
+
* in document order. Single walk; classification reads parsed attribute names rather
|
|
33
|
+
* than regex-matching the raw `[…]` text.
|
|
34
|
+
*
|
|
35
|
+
* Grammar (V2): each attribute (e.g. `[EventSubscriber(...)]`) is an `attribute_item`
|
|
36
|
+
* node that is a previous sibling of the procedure/trigger node. Walking stops at the
|
|
37
|
+
* first non-`attribute_item`, so a prior routine's attributes are never picked up.
|
|
38
|
+
*/
|
|
39
|
+
function classifyAndCollectAttributes(
|
|
40
|
+
node: SyntaxNode,
|
|
41
|
+
isTrigger: boolean,
|
|
42
|
+
): { kind: RoutineKind; attributes: string[]; attributesParsed: AttributeInfo[] } {
|
|
43
|
+
const attributes: string[] = [];
|
|
44
|
+
const attributesParsed: AttributeInfo[] = [];
|
|
45
|
+
let kind: RoutineKind = isTrigger ? "trigger" : "procedure";
|
|
46
|
+
let foundEventKind = false;
|
|
47
|
+
let sibling = node.previousSibling;
|
|
48
|
+
while (sibling && sibling.type === "attribute_item") {
|
|
49
|
+
attributes.unshift(sibling.text);
|
|
50
|
+
const info = attributeInfoFromNode(sibling);
|
|
51
|
+
if (info !== null) {
|
|
52
|
+
attributesParsed.unshift(info);
|
|
53
|
+
// First-match wins for kind, matching the old behavior (closest sibling wins).
|
|
54
|
+
// We walk closest-first so the same hit wins, but we *record* by unshifting
|
|
55
|
+
// to keep document order in the arrays.
|
|
56
|
+
if (!foundEventKind) {
|
|
57
|
+
const nameLc = info.name.toLowerCase();
|
|
58
|
+
if (nameLc === "eventsubscriber") {
|
|
59
|
+
kind = "event-subscriber";
|
|
60
|
+
foundEventKind = true;
|
|
61
|
+
} else if (nameLc === "integrationevent" || nameLc === "businessevent") {
|
|
62
|
+
kind = "event-publisher";
|
|
63
|
+
foundEventKind = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
sibling = sibling.previousSibling;
|
|
68
|
+
}
|
|
69
|
+
return { kind, attributes, attributesParsed };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read the `modifier` field on a procedure node (`local`/`internal`/`protected`).
|
|
74
|
+
* The grammar wraps the keyword in a `procedure_modifier` node whose text is the
|
|
75
|
+
* lowercase keyword. Returns undefined for triggers (no modifier) and for
|
|
76
|
+
* default-access procedures (AL's `public` default — modifier field absent).
|
|
77
|
+
*/
|
|
78
|
+
function classifyAccessModifier(node: SyntaxNode): ProcedureAccessModifier | undefined {
|
|
79
|
+
if (node.type !== "procedure") return undefined;
|
|
80
|
+
const modifier = node.childForFieldName("modifier");
|
|
81
|
+
if (modifier === null) return undefined;
|
|
82
|
+
const text = modifier.text.trim().toLowerCase();
|
|
83
|
+
if (text === "local") return "local";
|
|
84
|
+
if (text === "internal") return "internal";
|
|
85
|
+
if (text === "protected") return "protected";
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Find the code_block child of a procedure or trigger node. */
|
|
90
|
+
function findCodeBlock(node: SyntaxNode): SyntaxNode | null {
|
|
91
|
+
for (const child of node.namedChildren) {
|
|
92
|
+
if (child?.type === "code_block") return child;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read the return-type text of a procedure / trigger declaration. The grammar
|
|
99
|
+
* exposes return types as a direct `type_specification` child of `procedure`
|
|
100
|
+
* (parameter types live inside `parameter` nodes, never directly under
|
|
101
|
+
* `procedure`, so the first direct `type_specification` is unambiguously the
|
|
102
|
+
* return type). Triggers and void procedures return undefined.
|
|
103
|
+
*/
|
|
104
|
+
function getReturnTypeText(node: SyntaxNode): string | undefined {
|
|
105
|
+
for (const child of node.namedChildren) {
|
|
106
|
+
if (child !== null && child.type === "type_specification") return child.text;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** True if loop `outer`'s source range strictly contains loop `inner`'s range. */
|
|
112
|
+
function loopStrictlyContains(outer: LoopNode, inner: LoopNode): boolean {
|
|
113
|
+
if (outer.id === inner.id) return false;
|
|
114
|
+
const o = outer.sourceAnchor.range;
|
|
115
|
+
const i = inner.sourceAnchor.range;
|
|
116
|
+
const startsBefore =
|
|
117
|
+
o.startLine < i.startLine || (o.startLine === i.startLine && o.startColumn <= i.startColumn);
|
|
118
|
+
const endsAfter =
|
|
119
|
+
o.endLine > i.endLine || (o.endLine === i.endLine && o.endColumn >= i.endColumn);
|
|
120
|
+
return startsBefore && endsAfter;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Compute the maximum loop nesting depth from a routine's loop list.
|
|
125
|
+
* A loop's depth is 1 + the number of OTHER loops strictly containing it; the
|
|
126
|
+
* routine's nesting depth is the max across all loops (0 when there are no loops).
|
|
127
|
+
*/
|
|
128
|
+
function computeNestingDepth(loops: LoopNode[]): number {
|
|
129
|
+
let maxDepth = 0;
|
|
130
|
+
for (const loop of loops) {
|
|
131
|
+
const enclosing = loops.filter((other) => loopStrictlyContains(other, loop)).length;
|
|
132
|
+
const depth = 1 + enclosing;
|
|
133
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
134
|
+
}
|
|
135
|
+
return maxDepth;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Index every procedure and trigger in an object declaration node. */
|
|
139
|
+
export function indexRoutines(input: IndexRoutinesInput): Routine[] {
|
|
140
|
+
const { objectNode, object, sourceUnitId, modelInstanceId } = input;
|
|
141
|
+
const routines: Routine[] = [];
|
|
142
|
+
|
|
143
|
+
// Prune at match: AL has no nested procedures, so once we hit a procedure or
|
|
144
|
+
// trigger_declaration we don't descend into its body. Without this the discovery
|
|
145
|
+
// scan re-walks every routine body before extractOpsAndLoops walks it again.
|
|
146
|
+
const routineNodes = collectDescendants(
|
|
147
|
+
objectNode,
|
|
148
|
+
(n) => n.type === "procedure" || n.type === "trigger_declaration",
|
|
149
|
+
true,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
for (const node of routineNodes) {
|
|
153
|
+
const isTrigger = node.type === "trigger_declaration";
|
|
154
|
+
const nameNode = node.childForFieldName("name");
|
|
155
|
+
const name = nameNode ? stripQuotes(nameNode.text) : "";
|
|
156
|
+
if (!name) continue;
|
|
157
|
+
|
|
158
|
+
const { kind, attributes, attributesParsed } = classifyAndCollectAttributes(node, isTrigger);
|
|
159
|
+
const accessModifier = classifyAccessModifier(node);
|
|
160
|
+
|
|
161
|
+
// Parameters drive the canonical signature hash. Extract them first
|
|
162
|
+
// (no routineId required) so `normalizedSignatureHash` uses the structured
|
|
163
|
+
// form (`canonicalRoutineSignature`) — matching the ABI projection's hash
|
|
164
|
+
// for the same routine declared in a `.app` symbol package.
|
|
165
|
+
const parameters = extractParameters(node);
|
|
166
|
+
const returnTypeText = getReturnTypeText(node);
|
|
167
|
+
const canonical: CanonicalRoutineKey = {
|
|
168
|
+
appGuid: object.appGuid,
|
|
169
|
+
objectType: object.objectType,
|
|
170
|
+
objectNumber: object.objectNumber,
|
|
171
|
+
routineKind: kind,
|
|
172
|
+
routineName: name,
|
|
173
|
+
normalizedSignatureHash: sha256Hex(
|
|
174
|
+
canonicalRoutineSignature(name, parameters, returnTypeText),
|
|
175
|
+
),
|
|
176
|
+
};
|
|
177
|
+
const routineId = encodeRoutineId(canonical, modelInstanceId);
|
|
178
|
+
|
|
179
|
+
// Single body traversal: loops + record-ops + commits + call sites + field accesses
|
|
180
|
+
// all come from one DFS over the routine body. Record variables and parameters live
|
|
181
|
+
// on the procedure node (parameter_list, var_section) and are extracted separately —
|
|
182
|
+
// they need to be known before body walk so field accesses can be filtered to
|
|
183
|
+
// known record-var receivers.
|
|
184
|
+
const body = findCodeBlock(node);
|
|
185
|
+
const { recordVariables } = extractRecordVariablesAndParameters(node, routineId);
|
|
186
|
+
const recordVarNamesLc = new Set(recordVariables.map((v) => v.name.toLowerCase()));
|
|
187
|
+
const bodyResult = body
|
|
188
|
+
? extractBodyFeatures(
|
|
189
|
+
body,
|
|
190
|
+
routineId,
|
|
191
|
+
sourceUnitId,
|
|
192
|
+
recordVarNamesLc,
|
|
193
|
+
parameters,
|
|
194
|
+
recordVariables,
|
|
195
|
+
)
|
|
196
|
+
: {
|
|
197
|
+
loops: [],
|
|
198
|
+
operationSites: [],
|
|
199
|
+
recordOperations: [],
|
|
200
|
+
callSites: [],
|
|
201
|
+
fieldAccesses: [],
|
|
202
|
+
unreachableStatements: [],
|
|
203
|
+
hasBranching: false,
|
|
204
|
+
statementTree: undefined as ControlFlowNode | undefined,
|
|
205
|
+
identifierReferences: [] as string[],
|
|
206
|
+
varAssignments: [] as VarAssignment[],
|
|
207
|
+
conditionReferences: [] as ConditionReference[],
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Propagate per-record-variable temp state onto each RecordOperation. The body
|
|
211
|
+
// indexer doesn't know the declaring var's tempState (it sees only the receiver
|
|
212
|
+
// text), so it emits `tempState: {kind: "unknown"}`. We resolve here by matching
|
|
213
|
+
// op.recordVariableName (case-insensitive) to a declared RecordVariable and
|
|
214
|
+
// copying its tempState (also resolves the recordVariableId + tableId carry-over).
|
|
215
|
+
const recordVarByNameLc = new Map(recordVariables.map((rv) => [rv.name.toLowerCase(), rv]));
|
|
216
|
+
const recordOperations = bodyResult.recordOperations.map((op) => {
|
|
217
|
+
const rv = recordVarByNameLc.get(op.recordVariableName.toLowerCase());
|
|
218
|
+
if (rv === undefined) return op;
|
|
219
|
+
return {
|
|
220
|
+
...op,
|
|
221
|
+
recordVariableId: op.recordVariableId ?? rv.id,
|
|
222
|
+
tempState: rv.tempState,
|
|
223
|
+
...(rv.tableId !== undefined ? { tableId: op.tableId ?? rv.tableId } : {}),
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const variables = extractVariables(node, routineId, sourceUnitId, parameters, recordVariables);
|
|
228
|
+
|
|
229
|
+
const features: IntraproceduralFeatures = {
|
|
230
|
+
loops: bodyResult.loops,
|
|
231
|
+
operationSites: bodyResult.operationSites,
|
|
232
|
+
recordOperations,
|
|
233
|
+
callSites: bodyResult.callSites,
|
|
234
|
+
fieldAccesses: bodyResult.fieldAccesses,
|
|
235
|
+
recordVariables,
|
|
236
|
+
nestingDepth: computeNestingDepth(bodyResult.loops),
|
|
237
|
+
unreachableStatements: bodyResult.unreachableStatements,
|
|
238
|
+
hasBranching: bodyResult.hasBranching,
|
|
239
|
+
statementTree: bodyResult.statementTree,
|
|
240
|
+
identifierReferences: bodyResult.identifierReferences,
|
|
241
|
+
variables,
|
|
242
|
+
varAssignments: bodyResult.varAssignments,
|
|
243
|
+
conditionReferences: bodyResult.conditionReferences,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// `hasError` is a boolean getter property on SyntaxNode (not a method).
|
|
247
|
+
routines.push({
|
|
248
|
+
id: routineId,
|
|
249
|
+
canonical,
|
|
250
|
+
objectId: object.id,
|
|
251
|
+
name,
|
|
252
|
+
kind,
|
|
253
|
+
parameters,
|
|
254
|
+
attributes,
|
|
255
|
+
attributesParsed,
|
|
256
|
+
...(accessModifier !== undefined ? { accessModifier } : {}),
|
|
257
|
+
bodyAvailable: body !== null,
|
|
258
|
+
parseIncomplete: node.hasError,
|
|
259
|
+
// Hash the raw UTF-8 source bytes directly — equivalent to sha256Hex(node.text)
|
|
260
|
+
// for valid UTF-8 (which tree-sitter requires) and skips the JS-string allocation
|
|
261
|
+
// of the routine body. Big at 50k+ routines.
|
|
262
|
+
sourceHash: sha256HexBytes(node.textBytes),
|
|
263
|
+
sourceAnchor: {
|
|
264
|
+
sourceUnitId,
|
|
265
|
+
range: nodeToSourceRange(node),
|
|
266
|
+
enclosingRoutineId: routineId,
|
|
267
|
+
syntaxKind: node.type,
|
|
268
|
+
},
|
|
269
|
+
features,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// `collectDescendants` does not guarantee document order — sort by source position
|
|
274
|
+
// so callers get routines in the order they appear in the file.
|
|
275
|
+
routines.sort((a, b) => {
|
|
276
|
+
const ar = a.sourceAnchor.range;
|
|
277
|
+
const br = b.sourceAnchor.range;
|
|
278
|
+
return ar.startLine - br.startLine || ar.startColumn - br.startColumn;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return routines;
|
|
282
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// src/index/routine-signature.ts
|
|
2
|
+
// Canonical routine signature, used as the input to `sha256Hex` for
|
|
3
|
+
// `CanonicalRoutineKey.normalizedSignatureHash` on both sides:
|
|
4
|
+
// - native AL source (`routine-indexer.ts`)
|
|
5
|
+
// - .app dependency symbols (`dependency-projection.ts`)
|
|
6
|
+
//
|
|
7
|
+
// Pre-PR-4 the two paths produced *different* hashes for the same routine:
|
|
8
|
+
// native shred raw param-list text (whitespace-stripped, including names),
|
|
9
|
+
// ABI hashed type-only with return type. Same routine in primary source vs
|
|
10
|
+
// the same routine as a dep symbol therefore minted different `RoutineId`s —
|
|
11
|
+
// a latent inconsistency masked only because the two sides never co-resolve.
|
|
12
|
+
//
|
|
13
|
+
// AL overload resolution disambiguates routines by parameter *types*, not
|
|
14
|
+
// names, and return type is part of the signature. The canonical form below
|
|
15
|
+
// matches that: per-parameter `[var ]<type>`, joined with `;`, plus the
|
|
16
|
+
// return type, plus the lowercased routine name. Case-insensitive throughout
|
|
17
|
+
// (AL identifiers are case-insensitive).
|
|
18
|
+
|
|
19
|
+
import type { ParameterSymbol } from "../model/entities.ts";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build the deterministic text form hashed into `normalizedSignatureHash`.
|
|
23
|
+
* Stable across runs and across native/ABI sources for the same routine.
|
|
24
|
+
*
|
|
25
|
+
* Format:
|
|
26
|
+
* `<name-lc>(<param-spec>;<param-spec>;…):<return-lc>`
|
|
27
|
+
* where `<param-spec>` is `var <type-lc>` or `<type-lc>`.
|
|
28
|
+
*
|
|
29
|
+
* Trims surrounding whitespace from each `typeText` (a defensive normalization
|
|
30
|
+
* — tree-sitter typeSpec node text is already token-tight) but does NOT touch
|
|
31
|
+
* inner whitespace, because that may be meaningful inside `quoted_identifier`
|
|
32
|
+
* table names (`Record "Sales Line"` vs `Record "SalesLine"` are different
|
|
33
|
+
* tables). The parameter separator is the literal `;`, so per-parameter
|
|
34
|
+
* boundaries do not need text normalization.
|
|
35
|
+
*/
|
|
36
|
+
export function canonicalRoutineSignature(
|
|
37
|
+
name: string,
|
|
38
|
+
parameters: readonly ParameterSymbol[],
|
|
39
|
+
returnTypeText: string | undefined,
|
|
40
|
+
): string {
|
|
41
|
+
const params = parameters
|
|
42
|
+
.map((p) => `${p.isVar ? "var " : ""}${p.typeText.trim().toLowerCase()}`)
|
|
43
|
+
.join(";");
|
|
44
|
+
const ret = (returnTypeText ?? "").trim().toLowerCase();
|
|
45
|
+
return `${name.toLowerCase()}(${params}):${ret}`;
|
|
46
|
+
}
|