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,54 @@
|
|
|
1
|
+
import type { Uncertainty } from "../model/summary.ts";
|
|
2
|
+
|
|
3
|
+
/** Stable string key for an Uncertainty — kind plus whichever id field it carries. */
|
|
4
|
+
export function uncertaintyKey(u: Uncertainty): string {
|
|
5
|
+
if ("callsiteId" in u) return `${u.kind}|${u.callsiteId}`;
|
|
6
|
+
if ("operationId" in u) return `${u.kind}|${u.operationId}`;
|
|
7
|
+
return `${u.kind}|${u.routineId}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* De-duplicate a list of Uncertainty values by key (keep first seen), then sort by key.
|
|
12
|
+
* The sort uses `compareStrings` for locale-independent, byte-stable ordering.
|
|
13
|
+
*/
|
|
14
|
+
export function dedupeUncertainties(list: Uncertainty[]): Uncertainty[] {
|
|
15
|
+
const byKey = new Map<string, Uncertainty>();
|
|
16
|
+
for (const u of list) byKey.set(uncertaintyKey(u), u);
|
|
17
|
+
return [...byKey.values()].sort((a, b) => compareStrings(uncertaintyKey(a), uncertaintyKey(b)));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Locale-independent string comparator. Returns -1 / 0 / 1 based on JS byte order,
|
|
22
|
+
* ensuring deterministic sort output across all environments regardless of locale settings.
|
|
23
|
+
*/
|
|
24
|
+
export function compareStrings(a: string, b: string): number {
|
|
25
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compare two strings naturally: split each into runs of letters and digits, compare
|
|
30
|
+
* digit runs numerically and letter runs lexicographically. Stable for equal prefixes.
|
|
31
|
+
* Used for detector ids ("d2" < "d10") and other dN-style keys.
|
|
32
|
+
*/
|
|
33
|
+
export function compareNatural(a: string, b: string): number {
|
|
34
|
+
const re = /(\d+)|(\D+)/g;
|
|
35
|
+
const pa = a.match(re) ?? [];
|
|
36
|
+
const pb = b.match(re) ?? [];
|
|
37
|
+
const len = Math.min(pa.length, pb.length);
|
|
38
|
+
for (let i = 0; i < len; i++) {
|
|
39
|
+
const ta = pa[i] as string;
|
|
40
|
+
const tb = pb[i] as string;
|
|
41
|
+
const aIsNum = /^\d/.test(ta);
|
|
42
|
+
const bIsNum = /^\d/.test(tb);
|
|
43
|
+
if (aIsNum && bIsNum) {
|
|
44
|
+
const na = Number.parseInt(ta, 10);
|
|
45
|
+
const nb = Number.parseInt(tb, 10);
|
|
46
|
+
if (na !== nb) return na < nb ? -1 : 1;
|
|
47
|
+
} else if (aIsNum !== bIsNum) {
|
|
48
|
+
return aIsNum ? -1 : 1;
|
|
49
|
+
} else {
|
|
50
|
+
if (ta !== tb) return ta < tb ? -1 : 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return pa.length - pb.length;
|
|
54
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** SHA-256 hex digest of a string. */
|
|
4
|
+
export function sha256Hex(input: string): string {
|
|
5
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SHA-256 hex digest of a raw byte slice. Equivalent to `sha256Hex(utf8-decode(bytes))`
|
|
10
|
+
* when `bytes` is valid UTF-8 (Node's createHash treats strings as UTF-8). Skips the
|
|
11
|
+
* intermediate JS string allocation for callers that already hold a byte buffer (e.g.
|
|
12
|
+
* tree-sitter Node text via `tree.sourceBytes.subarray(start, end)`).
|
|
13
|
+
*/
|
|
14
|
+
export function sha256HexBytes(bytes: Uint8Array): string {
|
|
15
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* SHA-256 hex digest of an ordered list of strings.
|
|
20
|
+
* Uses a length-prefixed encoding so concatenation is unambiguous:
|
|
21
|
+
* ["ab","c"] and ["a","bc"] produce different digests.
|
|
22
|
+
*/
|
|
23
|
+
export function sha256OfStrings(parts: string[]): string {
|
|
24
|
+
const h = createHash("sha256");
|
|
25
|
+
for (const part of parts) {
|
|
26
|
+
h.update(String(part.length));
|
|
27
|
+
h.update(":");
|
|
28
|
+
h.update(part, "utf8");
|
|
29
|
+
}
|
|
30
|
+
return h.digest("hex");
|
|
31
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/index/attribute-from-node.ts
|
|
2
|
+
// Build a structured `AttributeInfo` from a tree-sitter `attribute_item` node.
|
|
3
|
+
//
|
|
4
|
+
// Grammar shape (tree-sitter-al):
|
|
5
|
+
// attribute_item
|
|
6
|
+
// '['
|
|
7
|
+
// attribute_content
|
|
8
|
+
// name: identifier
|
|
9
|
+
// arguments?: attribute_arguments
|
|
10
|
+
// '('
|
|
11
|
+
// attribute_argument_list?
|
|
12
|
+
// <_attribute_argument>, ...
|
|
13
|
+
// ')'
|
|
14
|
+
// ']'
|
|
15
|
+
//
|
|
16
|
+
// `_attribute_argument` is a closed choice — boolean / integer / string_literal /
|
|
17
|
+
// identifier / quoted_identifier / qualified_enum_value / database_reference /
|
|
18
|
+
// member_expression — all surfaced as typed nodes. We map each into a typed
|
|
19
|
+
// `AttributeArg`; consumers query through `findAttribute` / `stringArg` /
|
|
20
|
+
// `qualifiedArg` helpers instead of switching on positional indices.
|
|
21
|
+
|
|
22
|
+
import type { AttributeArg, AttributeArgKind, AttributeInfo } from "../model/attributes.ts";
|
|
23
|
+
import type { Node as SyntaxNode } from "../parser/native/wrapper.ts";
|
|
24
|
+
|
|
25
|
+
/** Strip surrounding single or double quotes from a string_literal / quoted_identifier. */
|
|
26
|
+
function stripQuoteChars(text: string): string {
|
|
27
|
+
if (text.length < 2) return text;
|
|
28
|
+
const first = text[0];
|
|
29
|
+
const last = text[text.length - 1];
|
|
30
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
31
|
+
return text.slice(1, -1);
|
|
32
|
+
}
|
|
33
|
+
return text;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Classify a grammar node type into the AttributeArgKind enum. */
|
|
37
|
+
function kindOf(nodeType: string): AttributeArgKind {
|
|
38
|
+
switch (nodeType) {
|
|
39
|
+
case "boolean":
|
|
40
|
+
case "integer":
|
|
41
|
+
case "string_literal":
|
|
42
|
+
case "identifier":
|
|
43
|
+
case "quoted_identifier":
|
|
44
|
+
case "qualified_enum_value":
|
|
45
|
+
case "database_reference":
|
|
46
|
+
case "member_expression":
|
|
47
|
+
return nodeType;
|
|
48
|
+
default:
|
|
49
|
+
return "unknown";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pull the unquoted/derived `value` and optional `qualifier`/`member` out of an arg node.
|
|
55
|
+
* Tree-sitter exposes the structurally-relevant pieces as field children
|
|
56
|
+
* (`enum_type`/`value` for `qualified_enum_value`, `keyword`/`table_name` for
|
|
57
|
+
* `database_reference`), so we read those directly — no further text shredding.
|
|
58
|
+
*/
|
|
59
|
+
function deriveValueParts(
|
|
60
|
+
node: SyntaxNode,
|
|
61
|
+
kind: AttributeArgKind,
|
|
62
|
+
text: string,
|
|
63
|
+
): { value?: string; qualifier?: string; member?: string } {
|
|
64
|
+
switch (kind) {
|
|
65
|
+
case "boolean":
|
|
66
|
+
case "integer":
|
|
67
|
+
case "identifier":
|
|
68
|
+
return { value: text };
|
|
69
|
+
case "string_literal":
|
|
70
|
+
case "quoted_identifier":
|
|
71
|
+
return { value: stripQuoteChars(text) };
|
|
72
|
+
case "qualified_enum_value": {
|
|
73
|
+
const qualifier = node.childForFieldName("enum_type")?.text;
|
|
74
|
+
const memberRaw = node.childForFieldName("value")?.text;
|
|
75
|
+
const member = memberRaw !== undefined ? stripQuoteChars(memberRaw) : undefined;
|
|
76
|
+
return {
|
|
77
|
+
value: member,
|
|
78
|
+
...(qualifier !== undefined ? { qualifier } : {}),
|
|
79
|
+
...(member !== undefined ? { member } : {}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
case "database_reference": {
|
|
83
|
+
const qualifier = node.childForFieldName("keyword")?.text;
|
|
84
|
+
const memberRaw = node.childForFieldName("table_name")?.text;
|
|
85
|
+
const member = memberRaw !== undefined ? stripQuoteChars(memberRaw) : undefined;
|
|
86
|
+
return {
|
|
87
|
+
value: member,
|
|
88
|
+
...(qualifier !== undefined ? { qualifier } : {}),
|
|
89
|
+
...(member !== undefined ? { member } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
case "member_expression":
|
|
93
|
+
case "unknown":
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Build an `AttributeArg` from a single argument node inside `attribute_argument_list`. */
|
|
99
|
+
function argFromNode(node: SyntaxNode): AttributeArg {
|
|
100
|
+
const kind = kindOf(node.type);
|
|
101
|
+
const text = node.text;
|
|
102
|
+
const parts = deriveValueParts(node, kind, text);
|
|
103
|
+
return {
|
|
104
|
+
kind,
|
|
105
|
+
text,
|
|
106
|
+
...(parts.value !== undefined ? { value: parts.value } : {}),
|
|
107
|
+
...(parts.qualifier !== undefined ? { qualifier: parts.qualifier } : {}),
|
|
108
|
+
...(parts.member !== undefined ? { member: parts.member } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build an `AttributeInfo` from an `attribute_item` (or `var_attribute_item`) node.
|
|
114
|
+
*
|
|
115
|
+
* Returns null only when the grammar shape is unrecognizable — a parse error
|
|
116
|
+
* inside the attribute. In that case callers should fall back to the raw text
|
|
117
|
+
* (kept on `Routine.attributes`) for diagnostics; semantic consumers will see
|
|
118
|
+
* the routine as if the attribute were absent, matching prior regex-miss behavior.
|
|
119
|
+
*/
|
|
120
|
+
export function attributeInfoFromNode(item: SyntaxNode): AttributeInfo | null {
|
|
121
|
+
const content = item.childForFieldName("attribute");
|
|
122
|
+
if (content === null) return null;
|
|
123
|
+
const nameNode = content.childForFieldName("name");
|
|
124
|
+
const name = nameNode?.text ?? "";
|
|
125
|
+
if (name === "") return null;
|
|
126
|
+
|
|
127
|
+
const args: AttributeArg[] = [];
|
|
128
|
+
const argsNode = content.childForFieldName("arguments");
|
|
129
|
+
if (argsNode !== null) {
|
|
130
|
+
// `attribute_arguments` wraps `(` `attribute_argument_list?` `)`. The list
|
|
131
|
+
// node is the single named child; its named children are the args.
|
|
132
|
+
const list = argsNode.namedChildren.find((c) => c?.type === "attribute_argument_list");
|
|
133
|
+
if (list) {
|
|
134
|
+
for (const child of list.namedChildren) {
|
|
135
|
+
if (child !== null) args.push(argFromNode(child));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { name, args, raw: item.text };
|
|
141
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// src/index/callee-from-node.ts
|
|
2
|
+
// Build a structured `Callee` from a tree-sitter `call_expression` node.
|
|
3
|
+
//
|
|
4
|
+
// Grammar shape (tree-sitter-al ≥ v2.5.2):
|
|
5
|
+
// call_expression
|
|
6
|
+
// function: identifier → bare procedure call
|
|
7
|
+
// | member_expression → method call OR object-run dispatch
|
|
8
|
+
// object: identifier / keyword_identifier / member_expression / ...
|
|
9
|
+
// member: identifier (`Run` for object-run; method name otherwise)
|
|
10
|
+
// argument_list
|
|
11
|
+
// <args>...
|
|
12
|
+
//
|
|
13
|
+
// Object-run dispatch (`Codeunit.Run(...)` / `Page.Run(...)` / `Report.Run(...)`)
|
|
14
|
+
// is unambiguous in the grammar because the object position is a
|
|
15
|
+
// `keyword_identifier` (wrapping `codeunit_keyword` / `page_keyword` /
|
|
16
|
+
// `report_keyword`) — distinct from a plain `identifier` like `Sales` in
|
|
17
|
+
// `Sales.Run()`. The first argument's shape — `database_reference` with a
|
|
18
|
+
// `table_name` of kind `quoted_identifier` (quoted name), `identifier`
|
|
19
|
+
// (unquoted name), or `integer` (numeric id) — pins down the target.
|
|
20
|
+
//
|
|
21
|
+
// No regex anywhere — every distinction comes from `node.type` / field children.
|
|
22
|
+
|
|
23
|
+
import type { Callee, ObjectRunKind } from "../model/callee.ts";
|
|
24
|
+
import type { Node as SyntaxNode } from "../parser/native/wrapper.ts";
|
|
25
|
+
|
|
26
|
+
/** Strip a single layer of surrounding double/single quotes. */
|
|
27
|
+
function stripQuoteChars(text: string): string {
|
|
28
|
+
if (text.length < 2) return text;
|
|
29
|
+
const first = text[0];
|
|
30
|
+
const last = text[text.length - 1];
|
|
31
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
32
|
+
return text.slice(1, -1);
|
|
33
|
+
}
|
|
34
|
+
return text;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* True when `objNode` is a `keyword_identifier` whose inner keyword node is
|
|
39
|
+
* one of `codeunit_keyword` / `page_keyword` / `report_keyword`. Returns the
|
|
40
|
+
* properly-cased object-kind label, or null if not an object-run prefix.
|
|
41
|
+
*
|
|
42
|
+
* Grammar fact: object-run prefixes use `keyword_identifier` (an alias around
|
|
43
|
+
* the bare keyword tokens), while a variable named `Sales` uses plain
|
|
44
|
+
* `identifier`. The two are syntactically distinct, so this check cannot
|
|
45
|
+
* misclassify a regular variable as a `.Run` dispatch.
|
|
46
|
+
*/
|
|
47
|
+
function objectRunKindOfReceiver(objNode: SyntaxNode): ObjectRunKind | null {
|
|
48
|
+
if (objNode.type !== "keyword_identifier") return null;
|
|
49
|
+
for (const child of objNode.namedChildren) {
|
|
50
|
+
if (child === null) continue;
|
|
51
|
+
switch (child.type) {
|
|
52
|
+
case "codeunit_keyword":
|
|
53
|
+
return "Codeunit";
|
|
54
|
+
case "page_keyword":
|
|
55
|
+
return "Page";
|
|
56
|
+
case "report_keyword":
|
|
57
|
+
return "Report";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Classify the first argument of a `.Run(...)` call into the object-run target
|
|
65
|
+
* fields. The grammar surfaces three node kinds inside `database_reference`'s
|
|
66
|
+
* `table_name` field:
|
|
67
|
+
* - `quoted_identifier` → quoted name (e.g. `Codeunit::"Sales-Post"`)
|
|
68
|
+
* - `identifier` → unquoted name (e.g. `Codeunit::SalesPost`)
|
|
69
|
+
* - `integer` → numeric id (e.g. `Codeunit::80`)
|
|
70
|
+
*
|
|
71
|
+
* Anything else — a variable, a function call, an arithmetic expression —
|
|
72
|
+
* leaves the target dynamic (`targetRef: undefined`).
|
|
73
|
+
*/
|
|
74
|
+
function classifyObjectRunFirstArg(
|
|
75
|
+
firstArg: SyntaxNode | undefined,
|
|
76
|
+
objectKind: ObjectRunKind,
|
|
77
|
+
): { targetType: ObjectRunKind; targetRef?: string; targetIsName: boolean } {
|
|
78
|
+
if (firstArg === undefined || firstArg.type !== "database_reference") {
|
|
79
|
+
return { targetType: objectKind, targetRef: undefined, targetIsName: false };
|
|
80
|
+
}
|
|
81
|
+
const tableNameNode = firstArg.childForFieldName("table_name");
|
|
82
|
+
if (tableNameNode === null) {
|
|
83
|
+
return { targetType: objectKind, targetRef: undefined, targetIsName: false };
|
|
84
|
+
}
|
|
85
|
+
const text = tableNameNode.text;
|
|
86
|
+
switch (tableNameNode.type) {
|
|
87
|
+
case "integer":
|
|
88
|
+
return { targetType: objectKind, targetRef: text, targetIsName: false };
|
|
89
|
+
case "quoted_identifier":
|
|
90
|
+
return { targetType: objectKind, targetRef: stripQuoteChars(text), targetIsName: true };
|
|
91
|
+
default:
|
|
92
|
+
// identifier / quoted-identifier-like contextual-keyword forms — unquoted name.
|
|
93
|
+
return { targetType: objectKind, targetRef: text, targetIsName: true };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Find the first positional argument of a `call_expression`. The
|
|
99
|
+
* `argument_list` is a named child whose own named children are the args.
|
|
100
|
+
* Returns undefined for an empty argument list (`Foo()`).
|
|
101
|
+
*/
|
|
102
|
+
function firstArgumentNode(callNode: SyntaxNode): SyntaxNode | undefined {
|
|
103
|
+
const argList = callNode.namedChildren.find((c) => c !== null && c.type === "argument_list");
|
|
104
|
+
if (argList === undefined || argList === null) return undefined;
|
|
105
|
+
for (const child of argList.namedChildren) {
|
|
106
|
+
if (child !== null) return child;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Build the `member`-shaped Callee for a `member_expression` (object + member),
|
|
113
|
+
* with the object-run dispatch upgrade when the receiver is a keyword. `callNode`
|
|
114
|
+
* is the enclosing `call_expression` when one exists — used to inspect the first
|
|
115
|
+
* argument for object-run targeting. When invoked on a parameterless method-call
|
|
116
|
+
* statement (`Customer.SetRecFilter;`, no surrounding `call_expression`), pass
|
|
117
|
+
* the `member_expression` itself as `callNode` and the target stays dynamic.
|
|
118
|
+
*/
|
|
119
|
+
function calleeFromMemberExpression(memberExpr: SyntaxNode, callNode: SyntaxNode | null): Callee {
|
|
120
|
+
const objNode = memberExpr.childForFieldName("object") ?? memberExpr.namedChildren[0] ?? null;
|
|
121
|
+
const memberNode = memberExpr.childForFieldName("member") ?? memberExpr.namedChildren[1] ?? null;
|
|
122
|
+
if (objNode === null || memberNode === null) return { kind: "unknown" };
|
|
123
|
+
|
|
124
|
+
const memberLc = memberNode.text.toLowerCase();
|
|
125
|
+
const objectKind = objectRunKindOfReceiver(objNode);
|
|
126
|
+
if (objectKind !== null && memberLc === "run") {
|
|
127
|
+
const firstArg = callNode !== null ? firstArgumentNode(callNode) : undefined;
|
|
128
|
+
const parts = classifyObjectRunFirstArg(firstArg, objectKind);
|
|
129
|
+
return {
|
|
130
|
+
kind: "object-run",
|
|
131
|
+
objectKind,
|
|
132
|
+
targetType: parts.targetType,
|
|
133
|
+
...(parts.targetRef !== undefined ? { targetRef: parts.targetRef } : {}),
|
|
134
|
+
targetIsName: parts.targetIsName,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
kind: "member",
|
|
140
|
+
receiver: objNode.text,
|
|
141
|
+
method: stripQuoteChars(memberNode.text),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Classify a call expression or bare-method-call shape into a structured `Callee`.
|
|
147
|
+
*
|
|
148
|
+
* Accepts either:
|
|
149
|
+
* - a `call_expression` node (the common case — `Foo(a, b)`, `Sales.Post(x)`,
|
|
150
|
+
* `Codeunit.Run(Codeunit::80)`); or
|
|
151
|
+
* - a `member_expression` node in statement position (`Customer.SetRecFilter;`
|
|
152
|
+
* — parameterless method call written without parens).
|
|
153
|
+
*
|
|
154
|
+
* Returns `{ kind: "unknown" }` for malformed shapes — mirrors the prior regex
|
|
155
|
+
* behavior of falling through to unknown rather than throwing.
|
|
156
|
+
*/
|
|
157
|
+
export function calleeFromNode(node: SyntaxNode): Callee {
|
|
158
|
+
if (node.type === "member_expression") {
|
|
159
|
+
return calleeFromMemberExpression(node, null);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (node.type !== "call_expression") return { kind: "unknown" };
|
|
163
|
+
|
|
164
|
+
const funcNode = node.childForFieldName("function") ?? node.namedChildren[0] ?? null;
|
|
165
|
+
if (funcNode === null) return { kind: "unknown" };
|
|
166
|
+
|
|
167
|
+
if (funcNode.type === "identifier") {
|
|
168
|
+
return { kind: "bare", name: stripQuoteChars(funcNode.text) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// `quoted_identifier` callee (rare — `"My Routine"`(...)) — still a bare call.
|
|
172
|
+
if (funcNode.type === "quoted_identifier") {
|
|
173
|
+
return { kind: "bare", name: stripQuoteChars(funcNode.text) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (funcNode.type === "member_expression") {
|
|
177
|
+
return calleeFromMemberExpression(funcNode, node);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { kind: "unknown" };
|
|
181
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { CapabilityConfidence, CapabilityFact, ValueSource } from "../../model/capability.ts";
|
|
2
|
+
import type { CoverageReason } from "../../model/coverage.ts";
|
|
3
|
+
import type { ExtractionContext } from "./extractor.ts";
|
|
4
|
+
import { classifyValueSource } from "./value-source.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Phase 0b-β background family extractor. Detects background-job kickoff calls:
|
|
8
|
+
* - `TaskScheduler.CreateTask(CodeunitId, ...)` — member callee, receiver = "TaskScheduler"
|
|
9
|
+
* - `Session.StartSession(out NewSessionID, CodeunitId, ...)` — member callee, receiver = "Session"
|
|
10
|
+
* - `StartSession(out NewSessionID, CodeunitId, ...)` — bare callee
|
|
11
|
+
*
|
|
12
|
+
* Emits one CapabilityFact per match:
|
|
13
|
+
* op: "start", resourceKind: "background"
|
|
14
|
+
* resourceArgSource: classifyValueSource on the codeunit-id argument
|
|
15
|
+
* - arg[0] for TaskScheduler.CreateTask
|
|
16
|
+
* - arg[1] for Session.StartSession / bare StartSession (arg[0] is the OUT session-id var)
|
|
17
|
+
* confidence derived from codeunit-id ValueSource kind
|
|
18
|
+
* provenance: "direct", via: "self", witnessCallsiteId: cs.id
|
|
19
|
+
*
|
|
20
|
+
* No BackgroundExtra is defined — `extra` is omitted.
|
|
21
|
+
*
|
|
22
|
+
* Never throws.
|
|
23
|
+
*/
|
|
24
|
+
export function extractBackground(ctx: ExtractionContext): {
|
|
25
|
+
facts: CapabilityFact[];
|
|
26
|
+
reasons: CoverageReason[];
|
|
27
|
+
} {
|
|
28
|
+
try {
|
|
29
|
+
const facts: CapabilityFact[] = [];
|
|
30
|
+
for (const cs of ctx.routine?.features?.callSites ?? []) {
|
|
31
|
+
const callee = cs.callee;
|
|
32
|
+
if (!callee) continue;
|
|
33
|
+
|
|
34
|
+
let codeunitArgIdx: number | undefined;
|
|
35
|
+
|
|
36
|
+
if (callee.kind === "member") {
|
|
37
|
+
const receiverLc = callee.receiver.toLowerCase();
|
|
38
|
+
const methodLc = callee.method.toLowerCase();
|
|
39
|
+
if (receiverLc === "taskscheduler" && methodLc === "createtask") {
|
|
40
|
+
codeunitArgIdx = 0;
|
|
41
|
+
} else if (receiverLc === "session" && methodLc === "startsession") {
|
|
42
|
+
codeunitArgIdx = 1;
|
|
43
|
+
}
|
|
44
|
+
} else if (callee.kind === "bare") {
|
|
45
|
+
if (typeof callee.name === "string" && callee.name.toLowerCase() === "startsession") {
|
|
46
|
+
codeunitArgIdx = 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (codeunitArgIdx === undefined) continue;
|
|
51
|
+
|
|
52
|
+
const codeunitArgInfo = cs.argumentInfos?.[codeunitArgIdx];
|
|
53
|
+
const codeunitArgSource: ValueSource =
|
|
54
|
+
codeunitArgInfo !== undefined
|
|
55
|
+
? classifyValueSource(codeunitArgInfo, ctx)
|
|
56
|
+
: { kind: "unknown" };
|
|
57
|
+
|
|
58
|
+
facts.push({
|
|
59
|
+
subject: ctx.routine.id,
|
|
60
|
+
op: "start",
|
|
61
|
+
resourceKind: "background",
|
|
62
|
+
resourceArgSource: codeunitArgSource,
|
|
63
|
+
confidence: confidenceFromSource(codeunitArgSource),
|
|
64
|
+
provenance: "direct",
|
|
65
|
+
via: "self",
|
|
66
|
+
witnessCallsiteId: cs.id,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return { facts, reasons: [] };
|
|
70
|
+
} catch {
|
|
71
|
+
return { facts: [], reasons: ["extraction-failed"] };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function confidenceFromSource(vs: ValueSource): CapabilityConfidence {
|
|
76
|
+
switch (vs.kind) {
|
|
77
|
+
case "literal":
|
|
78
|
+
case "enum":
|
|
79
|
+
return "static";
|
|
80
|
+
case "constant-var":
|
|
81
|
+
return confidenceFromSource(vs.initializer);
|
|
82
|
+
case "parameter":
|
|
83
|
+
return "userDynamic";
|
|
84
|
+
case "table-field":
|
|
85
|
+
return "configDynamic";
|
|
86
|
+
case "expression":
|
|
87
|
+
case "unknown":
|
|
88
|
+
return "unresolved";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { CapabilityFact } from "../../model/capability.ts";
|
|
2
|
+
import type { CoverageReason } from "../../model/coverage.ts";
|
|
3
|
+
import type { ExtractionContext } from "./extractor.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Phase 0b-β commit family extractor. Emits one CapabilityFact per Commit
|
|
7
|
+
* statement / call in the routine body.
|
|
8
|
+
*
|
|
9
|
+
* Detection: iterates ctx.routine.features.operationSites looking for
|
|
10
|
+
* kind === "commit". AL's `Commit;` and `Commit()` both produce an
|
|
11
|
+
* OperationSite with kind "commit" at L2 index time (confirmed by probe).
|
|
12
|
+
* They do NOT appear in callSites.
|
|
13
|
+
*
|
|
14
|
+
* The witness is witnessOperationId (the OperationSite.id), not
|
|
15
|
+
* witnessCallsiteId, because commit is classified as an operation rather
|
|
16
|
+
* than a general call.
|
|
17
|
+
*
|
|
18
|
+
* Never throws.
|
|
19
|
+
*/
|
|
20
|
+
export function extractCommit(ctx: ExtractionContext): {
|
|
21
|
+
facts: CapabilityFact[];
|
|
22
|
+
reasons: CoverageReason[];
|
|
23
|
+
} {
|
|
24
|
+
try {
|
|
25
|
+
const facts: CapabilityFact[] = [];
|
|
26
|
+
const operationSites = ctx.routine?.features?.operationSites ?? [];
|
|
27
|
+
for (const op of operationSites) {
|
|
28
|
+
if (op.kind === "commit") {
|
|
29
|
+
facts.push({
|
|
30
|
+
subject: ctx.routine.id,
|
|
31
|
+
op: "commit",
|
|
32
|
+
resourceKind: "transaction",
|
|
33
|
+
confidence: "static",
|
|
34
|
+
provenance: "direct",
|
|
35
|
+
via: "self",
|
|
36
|
+
witnessOperationId: op.id,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { facts, reasons: [] };
|
|
41
|
+
} catch {
|
|
42
|
+
return { facts: [], reasons: ["extraction-failed"] };
|
|
43
|
+
}
|
|
44
|
+
}
|