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,101 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { type Unzipped, unzipSync } from "fflate";
|
|
3
|
+
import { sha256Hex } from "../hash.ts";
|
|
4
|
+
import type { AppIdentity } from "../model/identity.ts";
|
|
5
|
+
|
|
6
|
+
export interface SymbolPackage {
|
|
7
|
+
identity: AppIdentity;
|
|
8
|
+
hasEmbeddedSource: boolean;
|
|
9
|
+
/** Relative paths of embedded .al files (empty when symbol-only). */
|
|
10
|
+
embeddedSourceFiles: string[];
|
|
11
|
+
/** Raw SymbolReference.json text — parsed in Phase 2 by the resolver. */
|
|
12
|
+
symbolReferenceJson: string | null;
|
|
13
|
+
/** Decoded embedded .al file contents, keyed by relative path. */
|
|
14
|
+
embeddedSource: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** BC .app files may carry a binary header before the ZIP. The ZIP starts at PK\x03\x04. */
|
|
18
|
+
function stripAppHeader(bytes: Uint8Array): Uint8Array {
|
|
19
|
+
// Find the ZIP local-file-header magic: 0x50 0x4B 0x03 0x04
|
|
20
|
+
const limit = Math.min(bytes.length - 4, 4096);
|
|
21
|
+
for (let i = 0; i < limit; i++) {
|
|
22
|
+
if (
|
|
23
|
+
bytes[i] === 0x50 &&
|
|
24
|
+
bytes[i + 1] === 0x4b &&
|
|
25
|
+
bytes[i + 2] === 0x03 &&
|
|
26
|
+
bytes[i + 3] === 0x04
|
|
27
|
+
) {
|
|
28
|
+
return i === 0 ? bytes : bytes.subarray(i);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return bytes; // assume it is already a plain ZIP
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Read attribute value from a single XML tag, tolerant of attribute order. */
|
|
35
|
+
function readXmlAttr(xml: string, tag: string, attr: string): string {
|
|
36
|
+
const tagMatch = xml.match(new RegExp(`<${tag}\\b[^>]*>`, "i"));
|
|
37
|
+
if (!tagMatch) return "";
|
|
38
|
+
const attrMatch = tagMatch[0].match(new RegExp(`${attr}\\s*=\\s*"([^"]*)"`, "i"));
|
|
39
|
+
return attrMatch?.[1] ?? "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const textDecoder = new TextDecoder("utf-8");
|
|
43
|
+
|
|
44
|
+
/** Read a .app symbol package: app identity, embedded-source flag, raw symbol JSON. */
|
|
45
|
+
export async function readSymbolPackage(appPath: string): Promise<SymbolPackage> {
|
|
46
|
+
const raw = new Uint8Array(readFileSync(appPath));
|
|
47
|
+
const packageHash = sha256Hex(Buffer.from(raw).toString("base64"));
|
|
48
|
+
const zipBytes = stripAppHeader(raw);
|
|
49
|
+
|
|
50
|
+
let entries: Unzipped;
|
|
51
|
+
try {
|
|
52
|
+
entries = unzipSync(zipBytes);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
throw new Error(`Failed to unzip .app at ${appPath}: ${(err as Error).message}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Entry keys may use either path separator; normalize to forward slashes.
|
|
58
|
+
const norm = (k: string) => k.replace(/\\/g, "/");
|
|
59
|
+
const byName = new Map<string, Uint8Array>();
|
|
60
|
+
for (const [k, v] of Object.entries(entries)) {
|
|
61
|
+
byName.set(norm(k).toLowerCase(), v);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const manifestBytes = byName.get("navxmanifest.xml") ?? byName.get("./navxmanifest.xml");
|
|
65
|
+
const manifestXml = manifestBytes ? textDecoder.decode(manifestBytes) : "";
|
|
66
|
+
|
|
67
|
+
const appGuid = readXmlAttr(manifestXml, "App", "Id");
|
|
68
|
+
const name = readXmlAttr(manifestXml, "App", "Name");
|
|
69
|
+
const publisher = readXmlAttr(manifestXml, "App", "Publisher");
|
|
70
|
+
const version = readXmlAttr(manifestXml, "App", "Version");
|
|
71
|
+
|
|
72
|
+
const embeddedSource: Record<string, string> = {};
|
|
73
|
+
for (const [k, v] of byName) {
|
|
74
|
+
if (k.endsWith(".al")) {
|
|
75
|
+
embeddedSource[k] = textDecoder.decode(v);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const embeddedSourceFiles = Object.keys(embeddedSource);
|
|
79
|
+
const hasEmbeddedSource = embeddedSourceFiles.length > 0;
|
|
80
|
+
|
|
81
|
+
const symbolRefBytes = byName.get("symbolreference.json") ?? byName.get("./symbolreference.json");
|
|
82
|
+
const symbolReferenceJson = symbolRefBytes ? textDecoder.decode(symbolRefBytes) : null;
|
|
83
|
+
|
|
84
|
+
const identity: AppIdentity = {
|
|
85
|
+
appGuid,
|
|
86
|
+
publisher,
|
|
87
|
+
name,
|
|
88
|
+
version,
|
|
89
|
+
packageHash,
|
|
90
|
+
symbolReferenceHash: symbolReferenceJson ? sha256Hex(symbolReferenceJson) : undefined,
|
|
91
|
+
sourceKind: hasEmbeddedSource ? "app-source" : "symbol-only",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
identity,
|
|
96
|
+
hasEmbeddedSource,
|
|
97
|
+
embeddedSourceFiles,
|
|
98
|
+
symbolReferenceJson,
|
|
99
|
+
embeddedSource,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import type { AttributeArg, AttributeArgKind, AttributeInfo } from "../model/attributes.ts";
|
|
2
|
+
import type { RoutineKind } from "../model/ids.ts";
|
|
3
|
+
|
|
4
|
+
/** A parameter signature as carried by SymbolReference.json — no per-run ids. */
|
|
5
|
+
export interface AbiParameter {
|
|
6
|
+
name: string;
|
|
7
|
+
typeText: string;
|
|
8
|
+
isVar: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A routine signature from SymbolReference.json. No body, no source anchor, no per-run id. */
|
|
12
|
+
export interface AbiRoutine {
|
|
13
|
+
name: string;
|
|
14
|
+
kind: RoutineKind;
|
|
15
|
+
/** "integration" | "business" | "unknown" — only meaningful when kind === "event-publisher". */
|
|
16
|
+
eventKind: "integration" | "business" | "unknown";
|
|
17
|
+
parameters: AbiParameter[];
|
|
18
|
+
returnTypeText?: string;
|
|
19
|
+
isLocal: boolean;
|
|
20
|
+
isInternal: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Reconstructed attribute strings, e.g. "[IntegrationEvent(False, False)]". Kept for
|
|
23
|
+
* back-compat / display; semantic consumers must read `attributesParsed` instead.
|
|
24
|
+
*/
|
|
25
|
+
attributes: string[];
|
|
26
|
+
/**
|
|
27
|
+
* Structured attributes synthesized from the structured `RawAttr` JSON — same model
|
|
28
|
+
* shape as the native AL path's `AttributeInfo[]`, so event-graph, attribute-parser,
|
|
29
|
+
* and detectors traverse one normalized representation regardless of routine source.
|
|
30
|
+
*/
|
|
31
|
+
attributesParsed: AttributeInfo[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** An ABI field — table column metadata. */
|
|
35
|
+
export interface AbiField {
|
|
36
|
+
fieldNumber: number;
|
|
37
|
+
name: string;
|
|
38
|
+
dataType: string;
|
|
39
|
+
fieldClass: "Normal" | "FlowField" | "FlowFilter";
|
|
40
|
+
isBlobLike: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** An ABI key — references fields by name; index is array position. */
|
|
44
|
+
export interface AbiKey {
|
|
45
|
+
name: string;
|
|
46
|
+
fieldNames: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** An ABI object — codeunit/page/table/etc. Tables additionally appear in `SymbolReferenceAbi.tables`. */
|
|
50
|
+
export interface AbiObject {
|
|
51
|
+
objectType: string; // "Codeunit" | "Table" | "Page" | "Report" | "XMLport" | "Query" | "Interface" | "Enum" | ...
|
|
52
|
+
objectNumber: number;
|
|
53
|
+
name: string;
|
|
54
|
+
routines: AbiRoutine[];
|
|
55
|
+
/**
|
|
56
|
+
* Codeunit Subtype property value from the SymbolReference JSON (e.g. "Install", "Upgrade").
|
|
57
|
+
* Undefined when not present in the JSON or for non-codeunit objects.
|
|
58
|
+
*/
|
|
59
|
+
objectSubtype?: string;
|
|
60
|
+
/**
|
|
61
|
+
* Page PageType property value from the SymbolReference JSON (e.g. "API", "List", "Card").
|
|
62
|
+
* Undefined when not present in the JSON or for non-page objects.
|
|
63
|
+
*/
|
|
64
|
+
pageType?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** An ABI table — physical table layout. */
|
|
68
|
+
export interface AbiTable {
|
|
69
|
+
objectNumber: number;
|
|
70
|
+
name: string;
|
|
71
|
+
fields: AbiField[];
|
|
72
|
+
keys: AbiKey[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The neutral DTO `parseSymbolReference` produces — no model entities, no anchors, no ids. */
|
|
76
|
+
export interface SymbolReferenceAbi {
|
|
77
|
+
appGuid: string;
|
|
78
|
+
name: string;
|
|
79
|
+
publisher: string;
|
|
80
|
+
version: string;
|
|
81
|
+
objects: AbiObject[];
|
|
82
|
+
tables: AbiTable[];
|
|
83
|
+
/** Set when the JSON could not be parsed; objects/tables are then empty. */
|
|
84
|
+
error?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const BLOB_LIKE = new Set(["blob", "media", "mediaset"]);
|
|
88
|
+
|
|
89
|
+
interface RawAttr {
|
|
90
|
+
Name?: string;
|
|
91
|
+
Arguments?: { Value?: unknown }[];
|
|
92
|
+
}
|
|
93
|
+
interface RawMethod {
|
|
94
|
+
Name?: string;
|
|
95
|
+
Parameters?: { Name?: string; IsVar?: boolean; TypeDefinition?: { Name?: string } }[];
|
|
96
|
+
ReturnTypeDefinition?: { Name?: string };
|
|
97
|
+
Attributes?: RawAttr[];
|
|
98
|
+
IsLocal?: boolean;
|
|
99
|
+
IsInternal?: boolean;
|
|
100
|
+
}
|
|
101
|
+
interface RawField {
|
|
102
|
+
Id?: number;
|
|
103
|
+
Name?: string;
|
|
104
|
+
TypeDefinition?: { Name?: string };
|
|
105
|
+
Properties?: { Name?: string; Value?: string }[];
|
|
106
|
+
}
|
|
107
|
+
interface RawKey {
|
|
108
|
+
Name?: string;
|
|
109
|
+
FieldNames?: string[];
|
|
110
|
+
}
|
|
111
|
+
interface RawObject {
|
|
112
|
+
Id?: number;
|
|
113
|
+
Name?: string;
|
|
114
|
+
Methods?: RawMethod[];
|
|
115
|
+
Fields?: RawField[];
|
|
116
|
+
Keys?: RawKey[];
|
|
117
|
+
Properties?: { Name?: string; Value?: string }[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Read a named property value from a SymbolReference JSON object's `Properties` array.
|
|
122
|
+
* Comparison is case-insensitive. Returns the `Value` string or undefined when absent.
|
|
123
|
+
*/
|
|
124
|
+
function rawObjectProperty(
|
|
125
|
+
properties: { Name?: string; Value?: string }[] | undefined,
|
|
126
|
+
propertyName: string,
|
|
127
|
+
): string | undefined {
|
|
128
|
+
if (!properties) return undefined;
|
|
129
|
+
const lc = propertyName.toLowerCase();
|
|
130
|
+
for (const p of properties) {
|
|
131
|
+
if ((p.Name ?? "").toLowerCase() === lc) return p.Value;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Reconstruct an attribute string from a raw Attributes entry, e.g. "[IntegrationEvent(False, False)]". */
|
|
137
|
+
function attrString(a: RawAttr): string {
|
|
138
|
+
const name = a.Name ?? "";
|
|
139
|
+
const args = (a.Arguments ?? []).map((x) => String(x.Value ?? "")).join(", ");
|
|
140
|
+
return args === "" ? `[${name}]` : `[${name}(${args})]`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Object-type keywords that distinguish a `database_reference` (e.g. `Codeunit::"X"`)
|
|
145
|
+
* from a generic `qualified_enum_value` (e.g. `ObjectType::Codeunit`). Mirrors the
|
|
146
|
+
* `object_type_keyword` alias in tree-sitter-al's grammar.
|
|
147
|
+
*/
|
|
148
|
+
const OBJECT_TYPE_KEYWORDS = new Set([
|
|
149
|
+
"database",
|
|
150
|
+
"page",
|
|
151
|
+
"report",
|
|
152
|
+
"codeunit",
|
|
153
|
+
"xmlport",
|
|
154
|
+
"query",
|
|
155
|
+
"table",
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
/** True if every char in `s` is `0-9` (and `s` is non-empty, optional leading minus). */
|
|
159
|
+
function isIntegerText(s: string): boolean {
|
|
160
|
+
if (s.length === 0) return false;
|
|
161
|
+
let i = 0;
|
|
162
|
+
if (s.charCodeAt(0) === 45 /* '-' */) {
|
|
163
|
+
i = 1;
|
|
164
|
+
if (i === s.length) return false;
|
|
165
|
+
}
|
|
166
|
+
for (; i < s.length; i++) {
|
|
167
|
+
const c = s.charCodeAt(i);
|
|
168
|
+
if (c < 48 || c > 57) return false;
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Classify a single `RawAttr.Arguments[].Value` into a typed `AttributeArg`. The
|
|
175
|
+
* SymbolReference JSON stores values as primitives (booleans/numbers) or already-
|
|
176
|
+
* tokenized AL text (`'name'`, `Codeunit::"X"`, `ObjectType::Codeunit`); we map
|
|
177
|
+
* each shape onto the same kinds tree-sitter-al exposes, so consumers see one
|
|
178
|
+
* model whether the routine came from native source or an `.app` symbol package.
|
|
179
|
+
*/
|
|
180
|
+
function classifyAbiArg(raw: unknown): AttributeArg {
|
|
181
|
+
if (typeof raw === "boolean") {
|
|
182
|
+
const text = raw ? "true" : "false";
|
|
183
|
+
return { kind: "boolean", text, value: text };
|
|
184
|
+
}
|
|
185
|
+
if (typeof raw === "number" && Number.isFinite(raw) && Number.isInteger(raw)) {
|
|
186
|
+
const text = String(raw);
|
|
187
|
+
return { kind: "integer", text, value: text };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const text = String(raw ?? "");
|
|
191
|
+
const trimmed = text.trim();
|
|
192
|
+
if (trimmed === "") return { kind: "unknown", text };
|
|
193
|
+
|
|
194
|
+
const lower = trimmed.toLowerCase();
|
|
195
|
+
if (lower === "true" || lower === "false") {
|
|
196
|
+
return { kind: "boolean", text: trimmed, value: lower };
|
|
197
|
+
}
|
|
198
|
+
if (isIntegerText(trimmed)) {
|
|
199
|
+
return { kind: "integer", text: trimmed, value: trimmed };
|
|
200
|
+
}
|
|
201
|
+
if (trimmed.length >= 2 && trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
202
|
+
return { kind: "string_literal", text: trimmed, value: trimmed.slice(1, -1) };
|
|
203
|
+
}
|
|
204
|
+
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
205
|
+
return { kind: "quoted_identifier", text: trimmed, value: trimmed.slice(1, -1) };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const colonIdx = trimmed.indexOf("::");
|
|
209
|
+
if (colonIdx > 0) {
|
|
210
|
+
const qualifier = trimmed.slice(0, colonIdx).trim();
|
|
211
|
+
const memberRaw = trimmed.slice(colonIdx + 2).trim();
|
|
212
|
+
const member =
|
|
213
|
+
memberRaw.length >= 2 && memberRaw.startsWith('"') && memberRaw.endsWith('"')
|
|
214
|
+
? memberRaw.slice(1, -1)
|
|
215
|
+
: memberRaw;
|
|
216
|
+
const kind: AttributeArgKind = OBJECT_TYPE_KEYWORDS.has(qualifier.toLowerCase())
|
|
217
|
+
? "database_reference"
|
|
218
|
+
: "qualified_enum_value";
|
|
219
|
+
return { kind, text: trimmed, value: member, qualifier, member };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Bare token — treat as identifier. .app values are well-formed, no shape check needed.
|
|
223
|
+
return { kind: "identifier", text: trimmed, value: trimmed };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Build an `AttributeInfo` from a structured `RawAttr` JSON entry. */
|
|
227
|
+
function abiAttributeInfo(a: RawAttr): AttributeInfo {
|
|
228
|
+
const name = a.Name ?? "";
|
|
229
|
+
const args = (a.Arguments ?? []).map((x) => classifyAbiArg(x.Value));
|
|
230
|
+
const raw = args.length === 0 ? `[${name}]` : `[${name}(${args.map((x) => x.text).join(", ")})]`;
|
|
231
|
+
return { name, args, raw };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Classify a method as a routine, deriving event-publisher kind from its attributes. */
|
|
235
|
+
function parseMethod(m: RawMethod): AbiRoutine {
|
|
236
|
+
const rawAttrs = m.Attributes ?? [];
|
|
237
|
+
const attributes = rawAttrs.map(attrString);
|
|
238
|
+
const attributesParsed = rawAttrs.map(abiAttributeInfo);
|
|
239
|
+
|
|
240
|
+
let kind: RoutineKind = "procedure";
|
|
241
|
+
let eventKind: AbiRoutine["eventKind"] = "unknown";
|
|
242
|
+
for (const info of attributesParsed) {
|
|
243
|
+
const nameLc = info.name.toLowerCase();
|
|
244
|
+
if (nameLc === "integrationevent") {
|
|
245
|
+
kind = "event-publisher";
|
|
246
|
+
eventKind = "integration";
|
|
247
|
+
} else if (nameLc === "businessevent") {
|
|
248
|
+
kind = "event-publisher";
|
|
249
|
+
eventKind = "business";
|
|
250
|
+
} else if (nameLc === "eventsubscriber") {
|
|
251
|
+
kind = "event-subscriber";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
name: m.Name ?? "",
|
|
256
|
+
kind,
|
|
257
|
+
eventKind,
|
|
258
|
+
parameters: (m.Parameters ?? []).map((p) => ({
|
|
259
|
+
name: p.Name ?? "",
|
|
260
|
+
typeText: p.TypeDefinition?.Name ?? "",
|
|
261
|
+
isVar: p.IsVar === true,
|
|
262
|
+
})),
|
|
263
|
+
returnTypeText: m.ReturnTypeDefinition?.Name,
|
|
264
|
+
isLocal: m.IsLocal === true,
|
|
265
|
+
isInternal: m.IsInternal === true,
|
|
266
|
+
attributes,
|
|
267
|
+
attributesParsed,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function parseField(f: RawField): AbiField {
|
|
272
|
+
const dataType = f.TypeDefinition?.Name ?? "";
|
|
273
|
+
let fieldClass: AbiField["fieldClass"] = "Normal";
|
|
274
|
+
for (const p of f.Properties ?? []) {
|
|
275
|
+
if ((p.Name ?? "").toLowerCase() === "fieldclass") {
|
|
276
|
+
if (/flowfield/i.test(p.Value ?? "")) fieldClass = "FlowField";
|
|
277
|
+
else if (/flowfilter/i.test(p.Value ?? "")) fieldClass = "FlowFilter";
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
fieldNumber: f.Id ?? 0,
|
|
282
|
+
name: f.Name ?? "",
|
|
283
|
+
dataType,
|
|
284
|
+
fieldClass,
|
|
285
|
+
isBlobLike: BLOB_LIKE.has((dataType.split("[")[0] ?? dataType).toLowerCase()),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Parse the raw SymbolReference.json text into the neutral ABI DTO. Never throws. */
|
|
290
|
+
export function parseSymbolReference(json: string): SymbolReferenceAbi {
|
|
291
|
+
let raw: Record<string, unknown>;
|
|
292
|
+
try {
|
|
293
|
+
raw = JSON.parse(json) as Record<string, unknown>;
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return {
|
|
296
|
+
appGuid: "",
|
|
297
|
+
name: "",
|
|
298
|
+
publisher: "",
|
|
299
|
+
version: "",
|
|
300
|
+
objects: [],
|
|
301
|
+
tables: [],
|
|
302
|
+
error: `SymbolReference.json parse failed: ${(err as Error).message}`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const objects: AbiObject[] = [];
|
|
307
|
+
const tables: AbiTable[] = [];
|
|
308
|
+
|
|
309
|
+
// Routine-bearing object arrays → AbiObject with routines.
|
|
310
|
+
const ROUTINE_BEARING: [key: string, objectType: string][] = [
|
|
311
|
+
["Codeunits", "Codeunit"],
|
|
312
|
+
["Pages", "Page"],
|
|
313
|
+
["Reports", "Report"],
|
|
314
|
+
["XmlPorts", "XMLport"],
|
|
315
|
+
["Queries", "Query"],
|
|
316
|
+
["Interfaces", "Interface"],
|
|
317
|
+
];
|
|
318
|
+
for (const [key, objectType] of ROUTINE_BEARING) {
|
|
319
|
+
for (const o of (raw[key] as RawObject[] | undefined) ?? []) {
|
|
320
|
+
const abiObject: AbiObject = {
|
|
321
|
+
objectType,
|
|
322
|
+
objectNumber: o.Id ?? 0,
|
|
323
|
+
name: o.Name ?? "",
|
|
324
|
+
routines: (o.Methods ?? []).map(parseMethod),
|
|
325
|
+
};
|
|
326
|
+
if (objectType === "Codeunit") {
|
|
327
|
+
const subtype = rawObjectProperty(o.Properties, "Subtype");
|
|
328
|
+
if (subtype !== undefined) abiObject.objectSubtype = subtype;
|
|
329
|
+
}
|
|
330
|
+
if (objectType === "Page") {
|
|
331
|
+
const pt = rawObjectProperty(o.Properties, "PageType");
|
|
332
|
+
if (pt !== undefined) abiObject.pageType = pt;
|
|
333
|
+
}
|
|
334
|
+
objects.push(abiObject);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Tables → both an AbiTable (layout) and an AbiObject (so the symbol table can resolve them).
|
|
339
|
+
for (const t of (raw.Tables as RawObject[] | undefined) ?? []) {
|
|
340
|
+
const objectNumber = t.Id ?? 0;
|
|
341
|
+
tables.push({
|
|
342
|
+
objectNumber,
|
|
343
|
+
name: t.Name ?? "",
|
|
344
|
+
fields: (t.Fields ?? []).map(parseField),
|
|
345
|
+
keys: (t.Keys ?? []).map((k) => ({ name: k.Name ?? "", fieldNames: k.FieldNames ?? [] })),
|
|
346
|
+
});
|
|
347
|
+
objects.push({
|
|
348
|
+
objectType: "Table",
|
|
349
|
+
objectNumber,
|
|
350
|
+
name: t.Name ?? "",
|
|
351
|
+
routines: (t.Methods ?? []).map(parseMethod),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Bare object arrays — no routines al-sem summarises; projected for resolution completeness.
|
|
356
|
+
const BARE: [key: string, objectType: string][] = [
|
|
357
|
+
["EnumTypes", "Enum"],
|
|
358
|
+
["ControlAddIns", "ControlAddIn"],
|
|
359
|
+
["PermissionSets", "PermissionSet"],
|
|
360
|
+
["PermissionSetExtensions", "PermissionSetExtension"],
|
|
361
|
+
["ReportExtensions", "ReportExtension"],
|
|
362
|
+
["DotNetPackages", "DotNetPackage"],
|
|
363
|
+
];
|
|
364
|
+
for (const [key, objectType] of BARE) {
|
|
365
|
+
for (const o of (raw[key] as RawObject[] | undefined) ?? []) {
|
|
366
|
+
objects.push({ objectType, objectNumber: o.Id ?? 0, name: o.Name ?? "", routines: [] });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
appGuid: String(raw.AppId ?? ""),
|
|
372
|
+
name: String(raw.Name ?? ""),
|
|
373
|
+
publisher: String(raw.Publisher ?? ""),
|
|
374
|
+
version: String(raw.Version ?? ""),
|
|
375
|
+
objects,
|
|
376
|
+
tables,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { filteredUnzip } from "./app-package-zip.ts";
|
|
3
|
+
|
|
4
|
+
/** Decode bytes as UTF-8 and strip a leading BOM if present. */
|
|
5
|
+
function decodeText(bytes: Uint8Array): string {
|
|
6
|
+
const text = new TextDecoder("utf-8").decode(bytes);
|
|
7
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Extract the SymbolReference.json text from raw .app bytes. Returns null if absent. */
|
|
11
|
+
export function extractSymbolReferenceJson(appBytes: Uint8Array): string | null {
|
|
12
|
+
const entries = filteredUnzip(appBytes, (name) =>
|
|
13
|
+
name.toLowerCase().endsWith("symbolreference.json"),
|
|
14
|
+
);
|
|
15
|
+
const key = Object.keys(entries)[0];
|
|
16
|
+
const bytes = key ? entries[key] : undefined;
|
|
17
|
+
return bytes === undefined ? null : decodeText(bytes);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Read SymbolReference.json from a .app on disk. Returns null if absent. Never throws. */
|
|
21
|
+
export function readSymbolReferenceJson(appPath: string): string | null {
|
|
22
|
+
try {
|
|
23
|
+
return extractSymbolReferenceJson(new Uint8Array(readFileSync(appPath)));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noUncheckedIndexedAccess": true,
|
|
13
|
+
"noFallthroughCasesInSwitch": true,
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"types": ["bun"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["src", "test"]
|
|
18
|
+
}
|