bc-atlas 0.12.0 → 0.14.0
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/CHANGELOG.md +57 -0
- package/README.md +17 -19
- package/package.json +1 -1
- package/src/analyzer.js +221 -13
- package/src/blueprint/components.js +14 -4
- package/src/blueprint/evaluate.js +64 -18
- package/src/blueprint/index.js +3 -1
- package/src/blueprint/report.js +68 -3
- package/src/blueprint/resolutions.js +72 -0
- package/src/commands/refactor.js +4 -1
- package/src/context-analysis.js +719 -0
- package/src/contract-analysis.js +372 -0
- package/src/graph/history.js +13 -3
- package/src/graph/lenses/dependency.js +20 -3
- package/src/health.js +84 -13
- package/src/insights.js +107 -4
- package/src/refactor/move.js +14 -2
- package/src/relation-aggregation.js +18 -1
- package/src/resolver.js +1 -0
- package/src/sarif.js +48 -6
- package/src/views.js +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,63 @@ All notable changes to BC Atlas will be documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
|
|
5
5
|
follows [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.14.0 - 2026-11-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- A root `.mcp.json` starts BC Atlas from the local checkout for direct MCP
|
|
12
|
+
tool calls in Copilot-compatible hosts. The MCP integration test uses the
|
|
13
|
+
same configuration, and the agent guide explains setup and server approval.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Git-history analysis waits for its `git cat-file` reader to exit before
|
|
18
|
+
removing temporary snapshots, avoiding file-lock failures on Windows.
|
|
19
|
+
|
|
20
|
+
## 0.13.0 - 2026-11-04
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `bca check` flags interface methods whose enum implementation can complete
|
|
25
|
+
silently: an empty body, a constant `true` return, or a conditional path
|
|
26
|
+
with no error (`contract-fallback`). Without configuration these are
|
|
27
|
+
informational; `check.contractExpectations` names the interface methods
|
|
28
|
+
that must not fall back silently and turns them into warnings. Unresolved
|
|
29
|
+
delegation, symbol-only bodies, and ambiguous signatures stay unknown
|
|
30
|
+
instead of counting as safe. See [Configuration](./docs/configuration.md).
|
|
31
|
+
- `check.contexts` summarizes the responsibilities of selected objects:
|
|
32
|
+
reads, persistent and temporary writes, commits, resolved calls, and global
|
|
33
|
+
state, direct and up to `maxCallDepth` calls away. `roles`,
|
|
34
|
+
`lazyReadMethods`, and `forbid` turn them into policy warnings; bounded or
|
|
35
|
+
unresolved evidence is reported with its limitations.
|
|
36
|
+
- Architecture component selectors accept `file:` (one file, globs allowed),
|
|
37
|
+
more specific than a namespace, folder, or ID range.
|
|
38
|
+
- Enum bindings keep `DefaultImplementation` and `UnknownValueImplementation`
|
|
39
|
+
as evidence, and calls keep their arguments.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- Health and architecture findings carry their basis (`configured-policy`,
|
|
44
|
+
`observed-code-fact`, or `heuristic` for a proposed architecture), a
|
|
45
|
+
primary location, all evidence, and limitations. Text and Markdown show up
|
|
46
|
+
to five locations; JSON and SARIF keep everything.
|
|
47
|
+
- Component cycles are explained by a representative path with the
|
|
48
|
+
dependencies behind each hop, and, where several providers call back into
|
|
49
|
+
the same orchestrator procedure, suggest extracting a shared service.
|
|
50
|
+
- `bca refactor move` defaults to the common parent folder of a component's
|
|
51
|
+
objects instead of the folder most of them are in.
|
|
52
|
+
- Records of pages and tables with `SourceTableTemporary` count as temporary,
|
|
53
|
+
and record parameters of unknown temporariness are no longer reported as
|
|
54
|
+
persistent writes.
|
|
55
|
+
- Release: one *Release* workflow publishes the npm package and the VS Code
|
|
56
|
+
extension under one version, after testing both, and attaches the `.vsix`
|
|
57
|
+
to the GitHub release.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- Health reports no longer print `architecture origin: undefined`, and
|
|
62
|
+
Markdown evidence cells with `|` no longer break the table.
|
|
63
|
+
|
|
7
64
|
## 0.12.0 - 2026-10-29
|
|
8
65
|
|
|
9
66
|
### Added
|
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ In VS Code, install the **BC Atlas** extension and open an AL project:
|
|
|
44
44
|
| **Export** | `bca export` | Everything the extension shows, as files: the BC Atlas pages as one offline HTML file to share (default), the Markdown report ([example](./examples/report/README.md)), the 3D explorer, one Markdown page per object, any of ten diagram views as D2, SVG, PNG, PDF, or JSON, with `--watch`, and the project as a knowledge graph in RDF with an OWL ontology ([Turtle, RDF/XML, OWL, N-Triples, JSON-LD](./docs/knowledge-graph.md)). |
|
|
45
45
|
| | `bca docs` | User guides and test cases from AL UI tests (`[SCENARIO]`, `[GIVEN]`, `[WHEN]`, `[THEN]`) as Markdown, HTML, DITA 1.3, or Azure DevOps CSV, with real captions and translations, a flow diagram per scenario, and BPMN 2.0 processes per use case. [Example](./examples/docs/index.md) |
|
|
46
46
|
| **Integrate** | `bca run` | One entry point for Git hooks, schedules, and pipelines; verifies, stages, or publishes generated files. Also available as a [GitHub Action](./docs/automation-and-ci.md#github-actions). |
|
|
47
|
-
| | `bca update` | "BC Atlas 0.
|
|
47
|
+
| | `bca update` | "BC Atlas 0.14.0 is available": release notes, one-step update, and regenerated hooks, tasks, and pipelines. Dependabot pull requests on request. |
|
|
48
48
|
| | `bca mcp` | A Model Context Protocol server for AI agents; `bca help --format json` is the versioned command contract. |
|
|
49
49
|
|
|
50
50
|
Run `bca` for the same overview in your terminal and `bca help <command>` for
|
|
@@ -259,27 +259,25 @@ lists planned work.
|
|
|
259
259
|
|
|
260
260
|
### Publishing
|
|
261
261
|
|
|
262
|
-
Releases are published manually through the `
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
262
|
+
Releases are published manually through the `Release` workflow
|
|
263
|
+
(`.github/workflows/publish-npm.yml`). Set the version of both packages with
|
|
264
|
+
`npm run version:set -- <version>`, merge it, then run the workflow on `main`
|
|
265
|
+
with that exact version. It rejects mismatched, already published, or already
|
|
266
|
+
tagged versions, and tests the npm package and the VS Code extension, the
|
|
267
|
+
packaged `.vsix` included, before anything is published. Then it publishes the
|
|
268
|
+
npm package with the selected dist-tag and, unless switched off, the extension
|
|
269
|
+
to the Visual Studio Marketplace and, with the secret `OVSX_PAT`, to Open VSX.
|
|
270
|
+
Last, it tags the commit as `v<version>`, so
|
|
267
271
|
`uses: SchulzOli/BC-Atlas@v<version>` resolves, and creates a GitHub release
|
|
268
|
-
with the version's `CHANGELOG.md` section
|
|
272
|
+
with the version's `CHANGELOG.md` section and the `.vsix`.
|
|
273
|
+
|
|
274
|
+
No token is stored for either registry. npm authenticates through
|
|
269
275
|
[trusted publishing](https://docs.npmjs.com/trusted-publishers): the package's
|
|
270
276
|
trusted publisher on npmjs.com is this repository's `publish-npm.yml`
|
|
271
|
-
workflow
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
extension` workflow: set `version` in `vscode/package.json`, then run the
|
|
276
|
-
workflow with that version. It tests and packages the extension with the
|
|
277
|
-
current engine and publishes it to the Visual Studio Marketplace as the
|
|
278
|
-
publisher `SchulzOli`, signed in with Microsoft Entra ID through GitHub's OIDC
|
|
279
|
-
token (no Marketplace token is stored; see the
|
|
280
|
-
[launch checklist](https://github.com/SchulzOli/BC-Atlas/blob/main/vscode/marketing/README.md#launch-checklist)), and, with
|
|
281
|
-
the secret `OVSX_PAT`, to Open VSX. Every CI run uploads the `.vsix` as the
|
|
282
|
-
`bc-atlas-vsix` artifact.
|
|
277
|
+
workflow. The Marketplace publisher `SchulzOli` is signed in with Microsoft
|
|
278
|
+
Entra ID through GitHub's OIDC token in the `marketplace` environment (see the
|
|
279
|
+
[launch checklist](https://github.com/SchulzOli/BC-Atlas/blob/main/vscode/marketing/README.md#launch-checklist)).
|
|
280
|
+
Every CI run uploads the `.vsix` as the `bc-atlas-vsix` artifact.
|
|
283
281
|
|
|
284
282
|
## Contributing and support
|
|
285
283
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bc-atlas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "BC Atlas maps Microsoft Dynamics 365 Business Central AL projects: architecture diagrams, health checks, reports, and documentation generated from source.",
|
|
5
5
|
"homepage": "https://github.com/SchulzOli/BC-Atlas#readme",
|
|
6
6
|
"bugs": {
|
package/src/analyzer.js
CHANGED
|
@@ -293,10 +293,18 @@ function enclosingMember(node, source, types) {
|
|
|
293
293
|
return undefined;
|
|
294
294
|
}
|
|
295
295
|
|
|
296
|
-
function implementationRelations(value, node, source, add) {
|
|
296
|
+
function implementationRelations(property, value, node, source, add) {
|
|
297
|
+
const bindingKind = {
|
|
298
|
+
implementation: "explicit",
|
|
299
|
+
defaultimplementation: "default",
|
|
300
|
+
unknownvalueimplementation: "unknownValue"
|
|
301
|
+
}[property];
|
|
302
|
+
if (!bindingKind) return;
|
|
297
303
|
const identifier = String.raw`(?:"((?:""|[^"])*)"|([A-Za-z_][\w.]*))`;
|
|
298
304
|
const pattern = new RegExp(`${identifier}\\s*=\\s*${identifier}`, "gu");
|
|
299
|
-
const member =
|
|
305
|
+
const member = bindingKind === "explicit"
|
|
306
|
+
? enclosingMember(node, source, ["enum_value_declaration"])
|
|
307
|
+
: undefined;
|
|
300
308
|
for (const match of value.matchAll(pattern)) {
|
|
301
309
|
const contract = (match[1] ?? match[2]).replaceAll('""', '"');
|
|
302
310
|
const target = (match[3] ?? match[4]).replaceAll('""', '"');
|
|
@@ -304,6 +312,7 @@ function implementationRelations(value, node, source, add) {
|
|
|
304
312
|
target,
|
|
305
313
|
targetType: "codeunit",
|
|
306
314
|
kind: "selects",
|
|
315
|
+
bindingKind,
|
|
307
316
|
contract,
|
|
308
317
|
member
|
|
309
318
|
});
|
|
@@ -496,8 +505,8 @@ function referencesFor(objectNode, source, file) {
|
|
|
496
505
|
if (name === "includedpermissionsets") {
|
|
497
506
|
includedPermissionSetRelations(value, node, add);
|
|
498
507
|
}
|
|
499
|
-
if (
|
|
500
|
-
implementationRelations(value, node, source, add);
|
|
508
|
+
if (["implementation", "defaultimplementation", "unknownvalueimplementation"].includes(name)) {
|
|
509
|
+
implementationRelations(name, value, node, source, add);
|
|
501
510
|
}
|
|
502
511
|
if (name === "runobject") {
|
|
503
512
|
const match = value.match(
|
|
@@ -533,6 +542,9 @@ function callsFor(node, source, file) {
|
|
|
533
542
|
? functionNode.childForFieldName("object")
|
|
534
543
|
: undefined;
|
|
535
544
|
const argumentsNode = callNode.childForFieldName("arguments");
|
|
545
|
+
const argumentsList = argumentsNode?.namedChildren.map((argument) =>
|
|
546
|
+
source.slice(argument.startIndex, argument.endIndex).trim()
|
|
547
|
+
) ?? [];
|
|
536
548
|
return {
|
|
537
549
|
name: cleanName(source.slice(
|
|
538
550
|
(memberNode ?? functionNode).startIndex,
|
|
@@ -543,6 +555,7 @@ function callsFor(node, source, file) {
|
|
|
543
555
|
: undefined,
|
|
544
556
|
receiverCall: receiverNode?.type === "call_expression" ? describe(receiverNode) : undefined,
|
|
545
557
|
arity: argumentsNode?.namedChildCount ?? 0,
|
|
558
|
+
...(argumentsList.length ? { arguments: argumentsList } : {}),
|
|
546
559
|
scope: node.startPosition.row + 1,
|
|
547
560
|
location: locationFor(callNode, file)
|
|
548
561
|
};
|
|
@@ -610,6 +623,149 @@ function variableType(node, source) {
|
|
|
610
623
|
return declaredType(type);
|
|
611
624
|
}
|
|
612
625
|
|
|
626
|
+
function fallbackStatements(node, source, file, conditional = false) {
|
|
627
|
+
if (!node) return [];
|
|
628
|
+
if (node.type === "statement_block") {
|
|
629
|
+
return node.namedChildren.flatMap((child) =>
|
|
630
|
+
fallbackStatements(child, source, file, conditional)
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
if (node.type === "if_statement") {
|
|
634
|
+
return [{
|
|
635
|
+
kind: "conditional",
|
|
636
|
+
then: fallbackStatements(node.childForFieldName("then_branch"), source, file, true),
|
|
637
|
+
else: node.childForFieldName("else_branch")
|
|
638
|
+
? fallbackStatements(node.childForFieldName("else_branch"), source, file, true)
|
|
639
|
+
: null,
|
|
640
|
+
location: locationFor(node, file)
|
|
641
|
+
}];
|
|
642
|
+
}
|
|
643
|
+
if (node.type === "case_statement") {
|
|
644
|
+
const body = node.childForFieldName("body");
|
|
645
|
+
const branches = (body?.namedChildren ?? [])
|
|
646
|
+
.filter((child) => child.type === "case_branch")
|
|
647
|
+
.map((branch) => fallbackStatements(
|
|
648
|
+
branch.childForFieldName("body"),
|
|
649
|
+
source,
|
|
650
|
+
file,
|
|
651
|
+
true
|
|
652
|
+
));
|
|
653
|
+
const elseBranch = node.namedChildren.find((child) => child.type === "case_else_branch");
|
|
654
|
+
if (elseBranch) {
|
|
655
|
+
branches.push(elseBranch.namedChildren
|
|
656
|
+
.filter((child) => child.type !== "else_keyword")
|
|
657
|
+
.flatMap((child) => fallbackStatements(child, source, file, true)));
|
|
658
|
+
}
|
|
659
|
+
return [{
|
|
660
|
+
kind: "conditional",
|
|
661
|
+
branches,
|
|
662
|
+
hasImplicitEmptyPath: !elseBranch,
|
|
663
|
+
location: locationFor(node, file)
|
|
664
|
+
}];
|
|
665
|
+
}
|
|
666
|
+
if (["while_statement", "for_statement", "repeat_statement"].includes(node.type)) {
|
|
667
|
+
const body = node.childForFieldName("body");
|
|
668
|
+
return [{
|
|
669
|
+
kind: "conditional",
|
|
670
|
+
branches: [fallbackStatements(body, source, file, true)],
|
|
671
|
+
hasImplicitEmptyPath: node.type !== "repeat_statement",
|
|
672
|
+
location: locationFor(node, file)
|
|
673
|
+
}];
|
|
674
|
+
}
|
|
675
|
+
if (node.type === "call_expression") {
|
|
676
|
+
const functionNode = node.childForFieldName("function");
|
|
677
|
+
const memberNode = functionNode?.type === "member_expression"
|
|
678
|
+
? functionNode.childForFieldName("member")
|
|
679
|
+
: undefined;
|
|
680
|
+
const receiverNode = functionNode?.type === "member_expression"
|
|
681
|
+
? functionNode.childForFieldName("object")
|
|
682
|
+
: undefined;
|
|
683
|
+
const target = functionNode
|
|
684
|
+
? cleanName(source.slice(
|
|
685
|
+
(memberNode ?? functionNode).startIndex,
|
|
686
|
+
(memberNode ?? functionNode).endIndex
|
|
687
|
+
))
|
|
688
|
+
: undefined;
|
|
689
|
+
const argumentsNode = node.childForFieldName("arguments");
|
|
690
|
+
return [{
|
|
691
|
+
kind: "call",
|
|
692
|
+
target,
|
|
693
|
+
receiver: receiverNode
|
|
694
|
+
? cleanName(source.slice(receiverNode.startIndex, receiverNode.endIndex))
|
|
695
|
+
: undefined,
|
|
696
|
+
arity: argumentsNode?.namedChildCount ?? 0,
|
|
697
|
+
directError: !receiverNode && target?.toLocaleLowerCase("en-US") === "error",
|
|
698
|
+
conditional,
|
|
699
|
+
location: locationFor(node, file)
|
|
700
|
+
}];
|
|
701
|
+
}
|
|
702
|
+
if (node.type === "exit_statement") {
|
|
703
|
+
const returnNode = node.childForFieldName("return_value");
|
|
704
|
+
return [{
|
|
705
|
+
kind: "return",
|
|
706
|
+
value: returnNode ? source.slice(returnNode.startIndex, returnNode.endIndex).trim() : undefined,
|
|
707
|
+
constant: Boolean(returnNode && [
|
|
708
|
+
"boolean",
|
|
709
|
+
"integer",
|
|
710
|
+
"decimal",
|
|
711
|
+
"string_literal"
|
|
712
|
+
].includes(returnNode.type)),
|
|
713
|
+
conditional,
|
|
714
|
+
location: locationFor(node, file)
|
|
715
|
+
}];
|
|
716
|
+
}
|
|
717
|
+
if (node.type === "assignment_statement") {
|
|
718
|
+
const targetNode = node.childForFieldName("left");
|
|
719
|
+
return [{
|
|
720
|
+
kind: "assignment",
|
|
721
|
+
target: targetNode
|
|
722
|
+
? cleanName(source.slice(targetNode.startIndex, targetNode.endIndex))
|
|
723
|
+
: undefined,
|
|
724
|
+
conditional,
|
|
725
|
+
location: locationFor(node, file)
|
|
726
|
+
}];
|
|
727
|
+
}
|
|
728
|
+
return [{
|
|
729
|
+
kind: "other",
|
|
730
|
+
conditional,
|
|
731
|
+
location: locationFor(node, file)
|
|
732
|
+
}];
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function fallbackFactsFor(node, source, file, hasBody) {
|
|
736
|
+
if (!hasBody) return undefined;
|
|
737
|
+
const codeBlock = node.namedChildren.find((child) =>
|
|
738
|
+
["code_block", "statement_block"].includes(child.type)
|
|
739
|
+
);
|
|
740
|
+
const body = codeBlock?.childForFieldName("body") ??
|
|
741
|
+
codeBlock?.namedChildren.find((child) => child.type === "statement_block");
|
|
742
|
+
return {
|
|
743
|
+
complete: !node.hasError && !codeBlock?.hasError,
|
|
744
|
+
statements: fallbackStatements(body, source, file),
|
|
745
|
+
...((node.hasError || codeBlock?.hasError)
|
|
746
|
+
? { limitations: ["The AL parser reported errors in this procedure body."] }
|
|
747
|
+
: {})
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function resolveFallbackCalls(statements, procedures) {
|
|
752
|
+
for (const statement of statements) {
|
|
753
|
+
if (statement.kind === "conditional") {
|
|
754
|
+
if (statement.then) resolveFallbackCalls(statement.then, procedures);
|
|
755
|
+
if (statement.else) resolveFallbackCalls(statement.else, procedures);
|
|
756
|
+
for (const branch of statement.branches ?? []) {
|
|
757
|
+
resolveFallbackCalls(branch, procedures);
|
|
758
|
+
}
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (statement.kind !== "call" || statement.directError) continue;
|
|
762
|
+
const candidates = procedures.get(`${statement.target?.toLocaleLowerCase("en-US")}:${statement.arity}`) ?? [];
|
|
763
|
+
const target = !statement.receiver && candidates.length === 1 ? candidates[0] : undefined;
|
|
764
|
+
statement.resolved = Boolean(target?.fallbackFacts?.complete);
|
|
765
|
+
if (target) statement.targetSignature = target.signature;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
613
769
|
function membersFor(objectNode, source, file) {
|
|
614
770
|
const procedures = [];
|
|
615
771
|
const fields = [];
|
|
@@ -624,18 +780,24 @@ function membersFor(objectNode, source, file) {
|
|
|
624
780
|
const name = fieldText(node, "name", source);
|
|
625
781
|
if (name) {
|
|
626
782
|
const returnTypeNode = node.childForFieldName("return_type");
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
783
|
+
const header = procedureHeader(node, source);
|
|
784
|
+
const returnTypeText = returnTypeNode
|
|
785
|
+
? source.slice(returnTypeNode.startIndex, returnTypeNode.endIndex).trim()
|
|
786
|
+
: header.match(/\)\s*:\s*([^;\r\n]+)$/u)?.[1]?.trim();
|
|
787
|
+
const returnType = declaredType(returnTypeText);
|
|
630
788
|
const attributes = [
|
|
631
789
|
...node.namedChildren
|
|
632
790
|
.filter((child) => child.type === "attribute_item")
|
|
633
791
|
.map((child) => source.slice(child.startIndex, child.endIndex)),
|
|
634
792
|
...attributesFor(node, source)
|
|
635
793
|
];
|
|
794
|
+
const hasBody = node.namedChildren.some((child) =>
|
|
795
|
+
["code_block", "statement_block"].includes(child.type)
|
|
796
|
+
);
|
|
797
|
+
const fallbackFacts = fallbackFactsFor(node, source, file, hasBody);
|
|
636
798
|
procedures.push({
|
|
637
799
|
name,
|
|
638
|
-
header
|
|
800
|
+
header,
|
|
639
801
|
visibility: node.type === "trigger_declaration"
|
|
640
802
|
? undefined
|
|
641
803
|
: fieldText(node, "modifier", source)?.toLowerCase() ?? "public",
|
|
@@ -647,6 +809,8 @@ function membersFor(objectNode, source, file) {
|
|
|
647
809
|
: attributes.some((attribute) => /\[(?:Integration|Business)Event\b/iu.test(attribute))
|
|
648
810
|
? "event"
|
|
649
811
|
: "procedure",
|
|
812
|
+
hasBody,
|
|
813
|
+
...(fallbackFacts ? { fallbackFacts } : {}),
|
|
650
814
|
location: locationFor(node, file),
|
|
651
815
|
calls: callsFor(node, source, file),
|
|
652
816
|
attributes,
|
|
@@ -825,6 +989,18 @@ function membersFor(objectNode, source, file) {
|
|
|
825
989
|
.map(({ order, location }) => ({ kind: "commit", order, location }));
|
|
826
990
|
procedure.transactionSegments = procedure.transactionBoundaries.length + 1;
|
|
827
991
|
}
|
|
992
|
+
const localProcedures = new Map();
|
|
993
|
+
for (const procedure of procedures) {
|
|
994
|
+
const key = `${procedure.name.toLocaleLowerCase("en-US")}:${procedure.arity}`;
|
|
995
|
+
const candidates = localProcedures.get(key) ?? [];
|
|
996
|
+
candidates.push(procedure);
|
|
997
|
+
localProcedures.set(key, candidates);
|
|
998
|
+
}
|
|
999
|
+
for (const procedure of procedures) {
|
|
1000
|
+
if (procedure.fallbackFacts) {
|
|
1001
|
+
resolveFallbackCalls(procedure.fallbackFacts.statements, localProcedures);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
828
1004
|
const transactionBoundaries = procedures.flatMap((procedure) =>
|
|
829
1005
|
procedure.transactionBoundaries.map((boundary) => ({
|
|
830
1006
|
...boundary,
|
|
@@ -854,8 +1030,8 @@ function dataOperationRelations(members, object) {
|
|
|
854
1030
|
.map((variable) => [variable.name.toLowerCase(), variable])
|
|
855
1031
|
);
|
|
856
1032
|
if (sourceTable) {
|
|
857
|
-
variables.set("rec", { target: sourceTable, temporary:
|
|
858
|
-
variables.set("xrec", { target: sourceTable, temporary:
|
|
1033
|
+
variables.set("rec", { target: sourceTable, temporary: object.sourceTableTemporary });
|
|
1034
|
+
variables.set("xrec", { target: sourceTable, temporary: object.sourceTableTemporary });
|
|
859
1035
|
}
|
|
860
1036
|
let transactionSegment = 1;
|
|
861
1037
|
for (const call of procedure.calls) {
|
|
@@ -874,7 +1050,10 @@ function dataOperationRelations(members, object) {
|
|
|
874
1050
|
kind,
|
|
875
1051
|
relationClass: "runtime",
|
|
876
1052
|
operation: call.name,
|
|
877
|
-
|
|
1053
|
+
recordVariable: call.receiver,
|
|
1054
|
+
temporary: variable.parameter && variable.temporary !== true
|
|
1055
|
+
? undefined
|
|
1056
|
+
: variable.temporary,
|
|
878
1057
|
member: procedure.signature,
|
|
879
1058
|
sourceProcedure: procedure.signature,
|
|
880
1059
|
transactionSegment,
|
|
@@ -885,6 +1064,24 @@ function dataOperationRelations(members, object) {
|
|
|
885
1064
|
return relations;
|
|
886
1065
|
}
|
|
887
1066
|
|
|
1067
|
+
function subscriberRelations(procedures) {
|
|
1068
|
+
const relations = [];
|
|
1069
|
+
for (const procedure of procedures) {
|
|
1070
|
+
for (const attribute of procedure.attributes ?? []) {
|
|
1071
|
+
const match = attribute.match(/^\[\s*EventSubscriber\s*\(\s*ObjectType::(Codeunit|Table|Page|Report|XmlPort)\s*,\s*(?:Codeunit|Database|Page|Report|XmlPort)::\s*(?:"((?:""|[^"])*)"|([^,\]\s]+))/iu);
|
|
1072
|
+
if (!match) continue;
|
|
1073
|
+
relations.push({
|
|
1074
|
+
target: (match[2] ?? match[3]).replaceAll('""', '"'),
|
|
1075
|
+
targetType: match[1].toLowerCase(),
|
|
1076
|
+
kind: "subscribes",
|
|
1077
|
+
member: procedure.name,
|
|
1078
|
+
location: procedure.location
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return relations;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
888
1085
|
function helperDataOperationRelations(members, directRelations) {
|
|
889
1086
|
const procedures = new Map();
|
|
890
1087
|
for (const procedure of members.procedures) {
|
|
@@ -971,7 +1168,18 @@ async function objectsFromSource(source, file) {
|
|
|
971
1168
|
});
|
|
972
1169
|
}
|
|
973
1170
|
const members = membersFor(node, source, file);
|
|
974
|
-
const
|
|
1171
|
+
const properties = propertiesFor(node, source);
|
|
1172
|
+
const sourceTableTemporary = /^true$/iu.test(
|
|
1173
|
+
propertyValueFor(node, source, "sourcetabletemporary") ?? "false"
|
|
1174
|
+
);
|
|
1175
|
+
relations.push(...subscriberRelations(members.procedures));
|
|
1176
|
+
const dataRelations = dataOperationRelations(members, {
|
|
1177
|
+
type,
|
|
1178
|
+
name,
|
|
1179
|
+
base,
|
|
1180
|
+
relations,
|
|
1181
|
+
sourceTableTemporary
|
|
1182
|
+
});
|
|
975
1183
|
relations.push(...dataRelations, ...helperDataOperationRelations(members, dataRelations));
|
|
976
1184
|
|
|
977
1185
|
const assignableValue = type === "permissionset"
|
|
@@ -987,7 +1195,7 @@ async function objectsFromSource(source, file) {
|
|
|
987
1195
|
namespace,
|
|
988
1196
|
usings,
|
|
989
1197
|
file,
|
|
990
|
-
properties
|
|
1198
|
+
properties,
|
|
991
1199
|
relations,
|
|
992
1200
|
procedures: members.procedures,
|
|
993
1201
|
fields: members.fields,
|
|
@@ -2,10 +2,11 @@ import { normalizeIdentifier } from "../resolver.js";
|
|
|
2
2
|
|
|
3
3
|
// A component is a named part of the target architecture (a future app, or a
|
|
4
4
|
// layer inside one). Objects are assigned by selectors; the most specific
|
|
5
|
-
// matching selector wins (object, then the longest namespace or
|
|
6
|
-
// the narrowest ID range, then module and app, then type, then *):
|
|
5
|
+
// matching selector wins (object, then file, then the longest namespace or
|
|
6
|
+
// folder, then the narrowest ID range, then module and app, then type, then *):
|
|
7
7
|
// namespace:Contoso.Sales the namespace and everything below it
|
|
8
8
|
// folder:src/Sales files below a folder (globs allowed)
|
|
9
|
+
// file:src/Sales/Order.al one file (globs allowed)
|
|
9
10
|
// app:Contoso Sales an app by name or ID
|
|
10
11
|
// module:Job Queue a module (nested app.json) by name or ID
|
|
11
12
|
// type:table an object type
|
|
@@ -52,7 +53,7 @@ function compileSingle(selector, { apps, modules, componentOf, layerOf }) {
|
|
|
52
53
|
if (text === "*" || text === "**") return () => 1;
|
|
53
54
|
const separator = text.indexOf(":");
|
|
54
55
|
if (separator < 1) {
|
|
55
|
-
throw new Error(`Invalid component selector "${text}"; use namespace:, folder:, app:, module:, type:, object:, id:, or *`);
|
|
56
|
+
throw new Error(`Invalid component selector "${text}"; use namespace:, folder:, file:, app:, module:, type:, object:, id:, or *`);
|
|
56
57
|
}
|
|
57
58
|
const kind = text.slice(0, separator).toLowerCase();
|
|
58
59
|
const value = text.slice(separator + 1).trim();
|
|
@@ -80,6 +81,15 @@ function compileSingle(selector, { apps, modules, componentOf, layerOf }) {
|
|
|
80
81
|
}
|
|
81
82
|
return (node) => Boolean(node.file) && (node.file === folder || node.file.startsWith(`${folder}/`)) && specificity;
|
|
82
83
|
}
|
|
84
|
+
if (kind === "file") {
|
|
85
|
+
const file = value.replaceAll("\\", "/").replace(/^\.?\//u, "");
|
|
86
|
+
const specificity = 200 + file.replaceAll("*", "").replaceAll("?", "").length;
|
|
87
|
+
const expression = isGlob(file) ? globToRegExp(file) : undefined;
|
|
88
|
+
return (node) => {
|
|
89
|
+
const candidate = String(node.file ?? "").replaceAll("\\", "/");
|
|
90
|
+
return (expression ? expression.test(candidate) : candidate === file) && specificity;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
83
93
|
if (kind === "app") {
|
|
84
94
|
return (node) => {
|
|
85
95
|
const app = apps.get(node.app);
|
|
@@ -118,7 +128,7 @@ function compileSingle(selector, { apps, modules, componentOf, layerOf }) {
|
|
|
118
128
|
return (node) => (!type || node.type === type.toLowerCase()) &&
|
|
119
129
|
[node.label, node.objectId].filter(Boolean).some((candidate) => normalizeIdentifier(candidate) === name) && 1000;
|
|
120
130
|
}
|
|
121
|
-
throw new Error(`Unsupported selector kind "${kind}"; use namespace, folder, app, module, type, object, or id${componentOf ? ", component, or layer" : ""}`);
|
|
131
|
+
throw new Error(`Unsupported selector kind "${kind}"; use namespace, folder, file, app, module, type, object, or id${componentOf ? ", component, or layer" : ""}`);
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
/** Apps and modules by id, for selectors that name them. */
|