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,129 @@
|
|
|
1
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
2
|
+
import { beforeAnchor } from "../engine/source-anchor.ts";
|
|
3
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
4
|
+
import { roleOf } from "../model/entities.ts";
|
|
5
|
+
import type { DetectorStats, EvidenceStep, Finding } from "../model/finding.ts";
|
|
6
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
7
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
8
|
+
import { toConfidence } from "./confidence.ts";
|
|
9
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ops that legitimately put a record into a "loaded / initialised" state in memory
|
|
13
|
+
* — after any of these, a subsequent `Modify` / `Validate` is sound.
|
|
14
|
+
* - `Get`, `Find*`, `Next`: SQL load.
|
|
15
|
+
* - `Init`: AL's in-memory initialiser for new records, used before Insert.
|
|
16
|
+
* - `Insert`: After Insert the row is in memory and current.
|
|
17
|
+
* - `Copy`: copies state from another record-var that itself was loaded.
|
|
18
|
+
*/
|
|
19
|
+
const LOAD_OPS: ReadonlySet<string> = new Set([
|
|
20
|
+
"Get",
|
|
21
|
+
"FindFirst",
|
|
22
|
+
"FindLast",
|
|
23
|
+
"FindSet",
|
|
24
|
+
"Find",
|
|
25
|
+
"Next",
|
|
26
|
+
"Init",
|
|
27
|
+
"Insert",
|
|
28
|
+
"Copy",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Ops that mutate state of the SPECIFIC current record and therefore require it to
|
|
33
|
+
* have been loaded first. Note `ModifyAll` is intentionally excluded — it operates
|
|
34
|
+
* on the filtered set, not the current record, and the standard pattern is
|
|
35
|
+
* `SetRange(…); ModifyAll(field, value)` with no prior Get/Find. Flagging
|
|
36
|
+
* `ModifyAll` produced ~73% of D11's false positives on real workspaces.
|
|
37
|
+
*/
|
|
38
|
+
const MUTATING_OPS: ReadonlySet<string> = new Set(["Modify", "Validate"]);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* D11 — flags Modify/ModifyAll/Validate on a record variable that was never loaded
|
|
42
|
+
* earlier in the same routine. Suppresses by-var parameter records (the caller is
|
|
43
|
+
* responsible for loading them).
|
|
44
|
+
*/
|
|
45
|
+
export function detectD11(
|
|
46
|
+
model: SemanticModel,
|
|
47
|
+
_graph: CombinedGraph,
|
|
48
|
+
_ctx: DetectorContext,
|
|
49
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
50
|
+
const findings: Finding[] = [];
|
|
51
|
+
let candidatesConsidered = 0;
|
|
52
|
+
let skippedParseIncomplete = 0;
|
|
53
|
+
let skippedParameter = 0;
|
|
54
|
+
|
|
55
|
+
for (const routine of model.routines) {
|
|
56
|
+
if (roleOf(routine) !== "primary") continue;
|
|
57
|
+
if (!routine.bodyAvailable) continue;
|
|
58
|
+
if (routine.parseIncomplete) {
|
|
59
|
+
skippedParseIncomplete++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
candidatesConsidered++;
|
|
63
|
+
|
|
64
|
+
const paramRecordNames = new Set(
|
|
65
|
+
routine.features.recordVariables
|
|
66
|
+
.filter((rv) => rv.isParameter)
|
|
67
|
+
.map((rv) => rv.name.toLowerCase()),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
for (const op of routine.features.recordOperations) {
|
|
71
|
+
if (!MUTATING_OPS.has(op.op)) continue;
|
|
72
|
+
const varKey = op.recordVariableName.toLowerCase();
|
|
73
|
+
if (paramRecordNames.has(varKey)) {
|
|
74
|
+
skippedParameter++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const loadedBefore = routine.features.recordOperations.some(
|
|
78
|
+
(other) =>
|
|
79
|
+
LOAD_OPS.has(other.op) &&
|
|
80
|
+
other.recordVariableName.toLowerCase() === varKey &&
|
|
81
|
+
beforeAnchor(other.sourceAnchor, op.sourceAnchor),
|
|
82
|
+
);
|
|
83
|
+
if (loadedBefore) continue;
|
|
84
|
+
|
|
85
|
+
const path: EvidenceStep[] = [
|
|
86
|
+
{
|
|
87
|
+
routineId: routine.id,
|
|
88
|
+
operationId: op.id,
|
|
89
|
+
sourceAnchor: op.sourceAnchor,
|
|
90
|
+
note: `${op.op} on ${op.recordVariableName} with no prior Get/Find in this routine`,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
const finding: Finding = {
|
|
94
|
+
id: `d11/${routine.id}/${op.id}`,
|
|
95
|
+
rootCauseKey: `d11/${routine.id}/${op.id}`,
|
|
96
|
+
detector: "d11-modify-without-get",
|
|
97
|
+
title: "Modify without Get",
|
|
98
|
+
rootCause: `${routine.name} calls ${op.op} on ${op.recordVariableName} but never loaded it — the record's state may be stale or partial.`,
|
|
99
|
+
severity: "medium",
|
|
100
|
+
confidence: toConfidence([], "likely"),
|
|
101
|
+
primaryLocation: op.sourceAnchor,
|
|
102
|
+
evidencePath: path,
|
|
103
|
+
affectedObjects: [routine.objectId],
|
|
104
|
+
affectedTables: op.tableId !== undefined ? [op.tableId] : [],
|
|
105
|
+
fixOptions: [
|
|
106
|
+
{
|
|
107
|
+
description:
|
|
108
|
+
"Load the record with Get / FindFirst before mutating, or pass it in as a var parameter from a caller that loaded it.",
|
|
109
|
+
safety: "high",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
provenance: [{ source: "tree-sitter" }],
|
|
113
|
+
};
|
|
114
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
115
|
+
findings.push(finding);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const stats: DetectorStats = {
|
|
120
|
+
detector: "d11-modify-without-get",
|
|
121
|
+
candidatesConsidered,
|
|
122
|
+
findingsEmitted: findings.length,
|
|
123
|
+
skipped: {
|
|
124
|
+
parseIncomplete: skippedParseIncomplete > 0 ? skippedParseIncomplete : undefined,
|
|
125
|
+
other: skippedParameter > 0 ? skippedParameter : undefined,
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
return { findings: findings.sort((a, b) => compareStrings(a.id, b.id)), stats };
|
|
129
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
2
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
3
|
+
import { roleOf } from "../model/entities.ts";
|
|
4
|
+
import type { DetectorStats, Finding } from "../model/finding.ts";
|
|
5
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
6
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
7
|
+
import { toConfidence } from "./confidence.ts";
|
|
8
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* D12 — IntegrationEvent published from primary-app code with zero subscribers anywhere
|
|
12
|
+
* in the workspace + dependency closure → info finding. Helps identify dead extensibility
|
|
13
|
+
* surfaces.
|
|
14
|
+
*/
|
|
15
|
+
export function detectD12(
|
|
16
|
+
model: SemanticModel,
|
|
17
|
+
_graph: CombinedGraph,
|
|
18
|
+
ctx: DetectorContext,
|
|
19
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
20
|
+
const findings: Finding[] = [];
|
|
21
|
+
const { routineById } = ctx;
|
|
22
|
+
const subsByEvent = new Map<string, number>();
|
|
23
|
+
for (const edge of model.eventGraph.edges) {
|
|
24
|
+
subsByEvent.set(edge.eventId, (subsByEvent.get(edge.eventId) ?? 0) + 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let candidatesConsidered = 0;
|
|
28
|
+
let skippedOther = 0;
|
|
29
|
+
for (const ev of model.eventGraph.events) {
|
|
30
|
+
if (ev.eventKind !== "integration") continue;
|
|
31
|
+
const pubRoutineId = ev.publisherRoutineId;
|
|
32
|
+
if (pubRoutineId === undefined) continue;
|
|
33
|
+
const pubRoutine = routineById.get(pubRoutineId);
|
|
34
|
+
if (pubRoutine === undefined) continue;
|
|
35
|
+
if (roleOf(pubRoutine) !== "primary") continue;
|
|
36
|
+
candidatesConsidered++;
|
|
37
|
+
if ((subsByEvent.get(ev.id) ?? 0) > 0) {
|
|
38
|
+
skippedOther++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const finding: Finding = {
|
|
43
|
+
id: `d12/${ev.id}`,
|
|
44
|
+
rootCauseKey: `d12/${ev.id}`,
|
|
45
|
+
detector: "d12-dead-integration-event",
|
|
46
|
+
title: "Integration event has no subscribers",
|
|
47
|
+
rootCause: `${pubRoutine.name} publishes an integration event that no subscriber across this workspace or its dependencies handles — the extensibility point is dead.`,
|
|
48
|
+
severity: "info",
|
|
49
|
+
confidence: toConfidence([], "likely"),
|
|
50
|
+
primaryLocation: pubRoutine.sourceAnchor,
|
|
51
|
+
evidencePath: [
|
|
52
|
+
{
|
|
53
|
+
routineId: pubRoutine.id,
|
|
54
|
+
sourceAnchor: pubRoutine.sourceAnchor,
|
|
55
|
+
note: `publishes ${pubRoutine.name}`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
affectedObjects: [pubRoutine.objectId],
|
|
59
|
+
affectedTables: [],
|
|
60
|
+
fixOptions: [
|
|
61
|
+
{
|
|
62
|
+
description:
|
|
63
|
+
"Either remove the event if it has no real extensibility purpose, or document why it exists for future subscribers.",
|
|
64
|
+
safety: "medium",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
provenance: [{ source: "tree-sitter" }],
|
|
68
|
+
};
|
|
69
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
70
|
+
findings.push(finding);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
findings: findings.sort((a, b) => compareStrings(a.id, b.id)),
|
|
74
|
+
stats: {
|
|
75
|
+
detector: "d12-dead-integration-event",
|
|
76
|
+
candidatesConsidered,
|
|
77
|
+
findingsEmitted: findings.length,
|
|
78
|
+
skipped: { other: skippedOther > 0 ? skippedOther : undefined },
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { parseRoutineAttributes } from "../engine/attribute-parser.ts";
|
|
2
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
3
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
4
|
+
import { roleOf } from "../model/entities.ts";
|
|
5
|
+
import type { DetectorStats, Finding } from "../model/finding.ts";
|
|
6
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
7
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
8
|
+
import { toConfidence } from "./confidence.ts";
|
|
9
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* D13 — Primary-app code calls a routine in a different app that is declared as
|
|
13
|
+
* `[InternalProc]` or has `internal` visibility. Crossing this boundary is not
|
|
14
|
+
* enforced by the BC compiler today but breaks on any minor version bump that the
|
|
15
|
+
* dependency author makes to their internal surface — a latent compile-break risk.
|
|
16
|
+
*
|
|
17
|
+
* Detection strategy: walk every resolved combined-graph edge; flag edges where
|
|
18
|
+
* (a) the caller's analysisRole is "primary", (b) caller and callee live in different
|
|
19
|
+
* apps (different appGuid), and (c) the callee routine carries `[InternalProc]` or
|
|
20
|
+
* its `accessModifier` is `"internal"` (set either from the AL `internal procedure`
|
|
21
|
+
* modifier or projected from `.app` symbol `IsInternal`).
|
|
22
|
+
*/
|
|
23
|
+
export function detectD13(
|
|
24
|
+
model: SemanticModel,
|
|
25
|
+
graph: CombinedGraph,
|
|
26
|
+
ctx: DetectorContext,
|
|
27
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
28
|
+
const findings: Finding[] = [];
|
|
29
|
+
const seen = new Set<string>();
|
|
30
|
+
const { routineById, objectsById } = ctx;
|
|
31
|
+
|
|
32
|
+
let candidatesConsidered = 0;
|
|
33
|
+
let skippedOther = 0;
|
|
34
|
+
for (const [, edges] of graph.edgesByFrom) {
|
|
35
|
+
for (const e of edges) {
|
|
36
|
+
const caller = routineById.get(e.from);
|
|
37
|
+
const callee = routineById.get(e.to);
|
|
38
|
+
if (!caller || !callee) continue;
|
|
39
|
+
if (roleOf(caller) !== "primary") continue;
|
|
40
|
+
candidatesConsidered++;
|
|
41
|
+
|
|
42
|
+
const callerObj = objectsById.get(caller.objectId);
|
|
43
|
+
const calleeObj = objectsById.get(callee.objectId);
|
|
44
|
+
if (!callerObj || !calleeObj) continue;
|
|
45
|
+
// Only flag cross-app boundaries
|
|
46
|
+
if (callerObj.appGuid === calleeObj.appGuid) {
|
|
47
|
+
skippedOther++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const attrs = parseRoutineAttributes(callee);
|
|
52
|
+
if (!attrs.internalProc && callee.accessModifier !== "internal") {
|
|
53
|
+
skippedOther++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const id = `d13/${e.from}/${e.callsiteId ?? "x"}`;
|
|
58
|
+
if (seen.has(id)) continue;
|
|
59
|
+
seen.add(id);
|
|
60
|
+
|
|
61
|
+
const cs = caller.features.callSites.find((c) => c.id === e.callsiteId);
|
|
62
|
+
const anchor = cs?.sourceAnchor ?? caller.sourceAnchor;
|
|
63
|
+
|
|
64
|
+
const finding: Finding = {
|
|
65
|
+
id,
|
|
66
|
+
rootCauseKey: id,
|
|
67
|
+
detector: "d13-cross-app-internal-call",
|
|
68
|
+
title: "Cross-extension call into an internal procedure",
|
|
69
|
+
rootCause: `${caller.name} calls ${callee.name} (app ${calleeObj.appGuid}), which is declared Internal. Crossing this boundary breaks encapsulation and can stop compiling on any minor version bump of the dependency.`,
|
|
70
|
+
severity: "high",
|
|
71
|
+
confidence: toConfidence([], "confirmed"),
|
|
72
|
+
primaryLocation: anchor,
|
|
73
|
+
evidencePath: [
|
|
74
|
+
{
|
|
75
|
+
routineId: caller.id,
|
|
76
|
+
callsiteId: e.callsiteId,
|
|
77
|
+
sourceAnchor: anchor,
|
|
78
|
+
note: `calls Internal ${callee.name} in app ${calleeObj.appGuid}`,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
affectedObjects: [callerObj.id, calleeObj.id].sort(),
|
|
82
|
+
affectedTables: [],
|
|
83
|
+
fixOptions: [
|
|
84
|
+
{
|
|
85
|
+
description:
|
|
86
|
+
"Use the dependency's public API instead, or request the routine be promoted to Public upstream.",
|
|
87
|
+
safety: "medium",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
provenance: [{ source: "tree-sitter" }],
|
|
91
|
+
};
|
|
92
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
93
|
+
findings.push(finding);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
findings: findings.sort((a, b) => compareStrings(a.id, b.id)),
|
|
98
|
+
stats: {
|
|
99
|
+
detector: "d13-cross-app-internal-call",
|
|
100
|
+
candidatesConsidered,
|
|
101
|
+
findingsEmitted: findings.length,
|
|
102
|
+
skipped: { other: skippedOther > 0 ? skippedOther : undefined },
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
2
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
3
|
+
import { roleOf } from "../model/entities.ts";
|
|
4
|
+
import type { DetectorStats, Finding } from "../model/finding.ts";
|
|
5
|
+
import type { RoutineId } from "../model/ids.ts";
|
|
6
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
7
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
8
|
+
import { toConfidence } from "./confidence.ts";
|
|
9
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* D14 — forward reachability from the "reachable roots" set (triggers, event subscribers,
|
|
13
|
+
* and the procedures we cannot prove are app-scoped: public/protected always; `internal`
|
|
14
|
+
* only when the workspace lists at least one `internalsVisibleTo` app).
|
|
15
|
+
*
|
|
16
|
+
* Flaggable candidates:
|
|
17
|
+
* - `local procedure` — scope-limited to the declaring object; always flaggable when
|
|
18
|
+
* unreached.
|
|
19
|
+
* - `internal procedure` — app-scoped when no `internalsVisibleTo` entries exist;
|
|
20
|
+
* flaggable in that case (otherwise treated as external API surface and not flagged).
|
|
21
|
+
*
|
|
22
|
+
* A local helper called only via a public wrapper is correctly reached, because the
|
|
23
|
+
* wrapper is itself a root. Suppresses test-helper objects (object name ending in
|
|
24
|
+
* Test/Tests) and roots themselves.
|
|
25
|
+
*
|
|
26
|
+
* **Page / PageExtension procedures are also suppressed**: AL page metadata can reference
|
|
27
|
+
* a same-object procedure from property expressions (`Caption = MyCaption()`,
|
|
28
|
+
* `Visible = ShowGroup`, `Enabled = IsEditable()`, action `OnAction` handlers, factbox
|
|
29
|
+
* lambdas, …). Al-sem's resolver does not yet traverse those property values into call
|
|
30
|
+
* edges, so we cannot prove a page-procedure unreachable — flagging it would emit ~50 %
|
|
31
|
+
* false-positive noise on real Continia workspaces. Same rationale for `Report` and
|
|
32
|
+
* `XmlPort` whose layout/section bindings reference procedures. Codeunit / Table /
|
|
33
|
+
* TableExtension procedures continue to be flagged because their call graph is fully
|
|
34
|
+
* modelled by the resolver.
|
|
35
|
+
*/
|
|
36
|
+
const OBJECT_TYPES_WITHOUT_FULL_CALL_GRAPH: ReadonlySet<string> = new Set([
|
|
37
|
+
"Page",
|
|
38
|
+
"PageExtension",
|
|
39
|
+
"Report",
|
|
40
|
+
"XmlPort",
|
|
41
|
+
"Query",
|
|
42
|
+
]);
|
|
43
|
+
export function detectD14(
|
|
44
|
+
model: SemanticModel,
|
|
45
|
+
graph: CombinedGraph,
|
|
46
|
+
ctx: DetectorContext,
|
|
47
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
48
|
+
const roots = ctx.reachableRoots;
|
|
49
|
+
const reachable = new Set<RoutineId>(roots);
|
|
50
|
+
const queue: RoutineId[] = [...roots];
|
|
51
|
+
while (queue.length > 0) {
|
|
52
|
+
const id = queue.shift();
|
|
53
|
+
if (id === undefined) break;
|
|
54
|
+
for (const e of graph.edgesByFrom.get(id) ?? []) {
|
|
55
|
+
if (!reachable.has(e.to)) {
|
|
56
|
+
reachable.add(e.to);
|
|
57
|
+
queue.push(e.to);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const findings: Finding[] = [];
|
|
63
|
+
const { objectsById } = ctx;
|
|
64
|
+
let candidatesConsidered = 0;
|
|
65
|
+
let skippedOther = 0;
|
|
66
|
+
let skippedNonLocal = 0;
|
|
67
|
+
let skippedPropertyExpressionHost = 0;
|
|
68
|
+
for (const r of model.routines) {
|
|
69
|
+
if (roleOf(r) !== "primary") continue;
|
|
70
|
+
if (!r.bodyAvailable) continue;
|
|
71
|
+
candidatesConsidered++;
|
|
72
|
+
if (reachable.has(r.id)) {
|
|
73
|
+
skippedOther++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// `local procedure` is always flaggable (scope-limited to its object). `internal` is
|
|
77
|
+
// flaggable only when the workspace declares no `internalsVisibleTo` apps — without
|
|
78
|
+
// any granted consumer, `internal` is effectively app-scoped. `public`/`protected`
|
|
79
|
+
// stay roots (potential external API surface).
|
|
80
|
+
const accessFlaggable =
|
|
81
|
+
r.accessModifier === "local" ||
|
|
82
|
+
(r.accessModifier === "internal" && !ctx.internalReachableExternally);
|
|
83
|
+
if (!accessFlaggable) {
|
|
84
|
+
skippedNonLocal++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const obj = objectsById.get(r.objectId);
|
|
88
|
+
if (obj?.name.match(/Tests?$/i)) {
|
|
89
|
+
skippedOther++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (obj !== undefined && OBJECT_TYPES_WITHOUT_FULL_CALL_GRAPH.has(obj.objectType)) {
|
|
93
|
+
// See OBJECT_TYPES_WITHOUT_FULL_CALL_GRAPH docs — al-sem's resolver does not
|
|
94
|
+
// model property-expression references on these object types yet.
|
|
95
|
+
skippedPropertyExpressionHost++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (roots.has(r.id)) {
|
|
99
|
+
skippedOther++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const accessNote =
|
|
103
|
+
r.accessModifier === "internal"
|
|
104
|
+
? " The workspace's app.json has no `internalsVisibleTo` entries, so no other app can call it."
|
|
105
|
+
: "";
|
|
106
|
+
const finding: Finding = {
|
|
107
|
+
id: `d14/${r.id}`,
|
|
108
|
+
rootCauseKey: `d14/${r.id}`,
|
|
109
|
+
detector: "d14-dead-routine",
|
|
110
|
+
title: "Routine is unreachable from any entry point",
|
|
111
|
+
rootCause: `${r.name} on ${obj?.name ?? r.objectId} is not called from any page action, trigger, OnRun, web service, or event subscriber in this app — appears to be dead code.${accessNote}`,
|
|
112
|
+
severity: "info",
|
|
113
|
+
confidence: toConfidence([], "possible"),
|
|
114
|
+
primaryLocation: r.sourceAnchor,
|
|
115
|
+
evidencePath: [
|
|
116
|
+
{
|
|
117
|
+
routineId: r.id,
|
|
118
|
+
sourceAnchor: r.sourceAnchor,
|
|
119
|
+
note: `${r.name} (no inbound edges from entry-point closure)`,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
affectedObjects: [r.objectId],
|
|
123
|
+
affectedTables: [],
|
|
124
|
+
fixOptions: [
|
|
125
|
+
{
|
|
126
|
+
description:
|
|
127
|
+
"Remove the routine if truly unused, or wire it up to an entry point if intended to be invoked.",
|
|
128
|
+
safety: "low",
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
provenance: [{ source: "tree-sitter" }],
|
|
132
|
+
};
|
|
133
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
134
|
+
findings.push(finding);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
findings: findings.sort((a, b) => compareStrings(a.id, b.id)),
|
|
138
|
+
stats: {
|
|
139
|
+
detector: "d14-dead-routine",
|
|
140
|
+
candidatesConsidered,
|
|
141
|
+
findingsEmitted: findings.length,
|
|
142
|
+
skipped: {
|
|
143
|
+
...(skippedOther > 0 ? { other: skippedOther } : {}),
|
|
144
|
+
...(skippedNonLocal > 0 ? { nonLocal: skippedNonLocal } : {}),
|
|
145
|
+
...(skippedPropertyExpressionHost > 0
|
|
146
|
+
? { propertyExpressionHost: skippedPropertyExpressionHost }
|
|
147
|
+
: {}),
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { parseRoutineAttributes } from "../engine/attribute-parser.ts";
|
|
2
|
+
import type { CombinedGraph } from "../engine/combined-graph.ts";
|
|
3
|
+
import { compareStrings } from "../engine/uncertainty-util.ts";
|
|
4
|
+
import { roleOf } from "../model/entities.ts";
|
|
5
|
+
import type { DetectorStats, Finding } from "../model/finding.ts";
|
|
6
|
+
import type { SemanticModel } from "../model/model.ts";
|
|
7
|
+
import { fingerprintOf } from "../projection/finding-fingerprint.ts";
|
|
8
|
+
import { toConfidence } from "./confidence.ts";
|
|
9
|
+
import type { DetectorContext } from "./detector-context.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* D16 — Primary-app code calls a routine whose `[Obsolete(...)]` attribute is present.
|
|
13
|
+
*
|
|
14
|
+
* Detection strategy: walk every resolved combined-graph edge; flag edges where
|
|
15
|
+
* (a) the caller's analysisRole is "primary", and (b) the callee routine carries an
|
|
16
|
+
* `[Obsolete(...)]` attribute.
|
|
17
|
+
*
|
|
18
|
+
* Severity is mapped from the obsolete state:
|
|
19
|
+
* - Pending → "info" (deprecated, should migrate)
|
|
20
|
+
* - Removed → "high" (call will break on the next version bump)
|
|
21
|
+
*
|
|
22
|
+
* Crosses app boundaries — catches calls into Base App's deprecation surface that
|
|
23
|
+
* per-file analyzers can't follow.
|
|
24
|
+
*/
|
|
25
|
+
export function detectD16(
|
|
26
|
+
model: SemanticModel,
|
|
27
|
+
graph: CombinedGraph,
|
|
28
|
+
ctx: DetectorContext,
|
|
29
|
+
): { findings: Finding[]; stats: DetectorStats } {
|
|
30
|
+
const findings: Finding[] = [];
|
|
31
|
+
const { routineById } = ctx;
|
|
32
|
+
|
|
33
|
+
let candidatesConsidered = 0;
|
|
34
|
+
let skippedOther = 0;
|
|
35
|
+
for (const [, edges] of graph.edgesByFrom) {
|
|
36
|
+
for (const e of edges) {
|
|
37
|
+
const caller = routineById.get(e.from);
|
|
38
|
+
const callee = routineById.get(e.to);
|
|
39
|
+
if (!caller || !callee) continue;
|
|
40
|
+
if (roleOf(caller) !== "primary") continue;
|
|
41
|
+
candidatesConsidered++;
|
|
42
|
+
|
|
43
|
+
const attrs = parseRoutineAttributes(callee);
|
|
44
|
+
if (attrs.obsoleteState === undefined) {
|
|
45
|
+
skippedOther++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const cs = caller.features.callSites.find((c) => c.id === e.callsiteId);
|
|
50
|
+
const anchor = cs?.sourceAnchor ?? caller.sourceAnchor;
|
|
51
|
+
const sev = attrs.obsoleteState === "Removed" ? "high" : "info";
|
|
52
|
+
|
|
53
|
+
const finding: Finding = {
|
|
54
|
+
id: `d16/${e.from}/${e.callsiteId ?? "x"}/${e.to}`,
|
|
55
|
+
rootCauseKey: `d16/${e.from}/${e.callsiteId ?? "x"}/${e.to}`,
|
|
56
|
+
detector: "d16-obsolete-routine-call",
|
|
57
|
+
title: `Call to obsolete routine (${attrs.obsoleteState})`,
|
|
58
|
+
rootCause: `${caller.name} calls ${callee.name}${attrs.obsoleteReason ? ` — ${attrs.obsoleteReason}` : ""}.`,
|
|
59
|
+
severity: sev,
|
|
60
|
+
confidence: toConfidence([], "confirmed"),
|
|
61
|
+
primaryLocation: anchor,
|
|
62
|
+
evidencePath: [
|
|
63
|
+
{
|
|
64
|
+
routineId: caller.id,
|
|
65
|
+
callsiteId: e.callsiteId,
|
|
66
|
+
sourceAnchor: anchor,
|
|
67
|
+
note: `calls ${attrs.obsoleteState} ${callee.name}`,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
affectedObjects: [caller.objectId, callee.objectId].sort(),
|
|
71
|
+
affectedTables: [],
|
|
72
|
+
fixOptions: [
|
|
73
|
+
{
|
|
74
|
+
description:
|
|
75
|
+
attrs.obsoleteReason ?? "Replace the call with the documented successor API.",
|
|
76
|
+
safety: "high",
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
provenance: [{ source: "tree-sitter" }],
|
|
80
|
+
};
|
|
81
|
+
finding.fingerprint = fingerprintOf(finding, model);
|
|
82
|
+
findings.push(finding);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
findings: findings.sort((a, b) => compareStrings(a.id, b.id)),
|
|
87
|
+
stats: {
|
|
88
|
+
detector: "d16-obsolete-routine-call",
|
|
89
|
+
candidatesConsidered,
|
|
90
|
+
findingsEmitted: findings.length,
|
|
91
|
+
skipped: { other: skippedOther > 0 ? skippedOther : undefined },
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|