d365fo-mcp 1.6.0 → 1.7.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/README.md +1 -1
- package/bridge/D365MetadataBridge/Services/MetadataWriteService.cs +54 -1
- package/dist/bridge/bridgeAdapter.js +1 -1
- package/dist/metadata/modelDescriptor.d.ts +53 -0
- package/dist/metadata/modelDescriptor.js +129 -0
- package/dist/metadata/symbolIndex.js +4 -6
- package/dist/metadata/types.d.ts +8 -0
- package/dist/metadata/xmlParser.d.ts +5 -1
- package/dist/metadata/xmlParser.js +10 -2
- package/dist/metadata/xppDeclaration.d.ts +22 -0
- package/dist/metadata/xppDeclaration.js +36 -2
- package/dist/scripts/build-database.js +10 -6
- package/dist/scripts/build-fts.js +10 -6
- package/dist/scripts/extract-metadata.js +17 -4
- package/dist/server/toolSchemas/d365foFile.js +5 -5
- package/dist/tools/buildProject.d.ts +10 -0
- package/dist/tools/buildProject.js +49 -38
- package/dist/tools/compileLabels.d.ts +62 -0
- package/dist/tools/compileLabels.js +239 -0
- package/dist/tools/createD365File.js +25 -3
- package/dist/tools/dataEntityInfo.d.ts +9 -12
- package/dist/tools/dataEntityInfo.js +12 -3
- package/dist/tools/dataEntityXml.d.ts +60 -0
- package/dist/tools/dataEntityXml.js +165 -21
- package/dist/tools/enumInfo.d.ts +10 -3
- package/dist/tools/enumInfo.js +69 -10
- package/dist/tools/formInfo.js +11 -0
- package/dist/tools/generateD365Xml.d.ts +157 -0
- package/dist/tools/generateD365Xml.js +30 -5
- package/dist/tools/queryInfo.d.ts +10 -3
- package/dist/tools/queryInfo.js +123 -10
- package/dist/tools/reportInfo.js +42 -14
- package/dist/tools/resolveReferences.d.ts +14 -1
- package/dist/tools/resolveReferences.js +160 -14
- package/dist/tools/runBpCheck.js +18 -0
- package/dist/tools/updateSymbolIndex.js +3 -4
- package/dist/tools/validateCode.js +2 -1
- package/dist/tools/viewInfo.d.ts +15 -4
- package/dist/tools/viewInfo.js +162 -9
- package/dist/tools/xppKnowledge.js +53 -9
- package/dist/utils/axTablePropertyOrder.d.ts +1 -1
- package/dist/utils/axTablePropertyOrder.js +5 -0
- package/dist/utils/frameworkBin.d.ts +19 -0
- package/dist/utils/frameworkBin.js +48 -0
- package/dist/utils/indexedXmlLookup.d.ts +54 -0
- package/dist/utils/indexedXmlLookup.js +103 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
|
11
11
|
[](docs/TESTING.md)
|
|
12
12
|
<!-- coverage-badge:start -->
|
|
13
|
-
[](eval/COVERAGE.md) [](eval/COVERAGE.md) [](eval/COVERAGE.md)
|
|
14
14
|
<!-- coverage-badge:end -->
|
|
15
15
|
|
|
16
16
|
*Grounded AI development for Dynamics 365 Finance & Operations — works with GitHub Copilot and Claude Code*
|
|
@@ -1504,7 +1504,7 @@ namespace D365MetadataBridge.Services
|
|
|
1504
1504
|
?? throw new ArgumentException($"Table '{objectName}' not found");
|
|
1505
1505
|
var msi = GetModelSaveInfoForObject(_provider.Tables, objectName);
|
|
1506
1506
|
if (!SetAxTableProperty(obj, propertyPath, propertyValue))
|
|
1507
|
-
throw new ArgumentException($"Unknown AxTable property '{propertyPath}' — nothing was written. Supported: label, developerDocumentation, configurationKey, formRef, tableGroup, cacheLookup, clusteredIndex, primaryIndex, replacementKey, saveDataPerCompany, tableType, supportInheritance, instanceRelationType, extends, titleField1, titleField2.");
|
|
1507
|
+
throw new ArgumentException($"Unknown AxTable property '{propertyPath}' — nothing was written. Supported: label, developerDocumentation, configurationKey, formRef, tableGroup, cacheLookup, clusteredIndex, primaryIndex, replacementKey, saveDataPerCompany, allowRowVersionChangeTracking, createdBy, createdDateTime, createdTransactionId, modifiedBy, modifiedDateTime, modifiedTransactionId, tableType, supportInheritance, instanceRelationType, extends, titleField1, titleField2.");
|
|
1508
1508
|
((IMetaTableProvider)_provider.Tables).Update(obj, msi);
|
|
1509
1509
|
return new { success = true, operation = "modify-property", objectType, objectName, propertyPath, propertyValue, api = "Update" };
|
|
1510
1510
|
}
|
|
@@ -1548,6 +1548,16 @@ namespace D365MetadataBridge.Services
|
|
|
1548
1548
|
((IMetaViewProvider)_provider.Views).Update(obj, msi);
|
|
1549
1549
|
return new { success = true, operation = "modify-property", objectType, objectName, propertyPath, propertyValue, api = "Update" };
|
|
1550
1550
|
}
|
|
1551
|
+
case "data-entity":
|
|
1552
|
+
{
|
|
1553
|
+
var obj = _provider.DataEntityViews.Read(objectName)
|
|
1554
|
+
?? throw new ArgumentException($"Data entity '{objectName}' not found");
|
|
1555
|
+
var msi = GetModelSaveInfoForObject(_provider.DataEntityViews, objectName);
|
|
1556
|
+
if (!SetAxDataEntityViewProperty(obj, propertyPath, propertyValue))
|
|
1557
|
+
throw new ArgumentException($"Unknown AxDataEntityView property '{propertyPath}' — nothing was written. Supported: label, developerDocumentation, primaryKey, isPublic, publicEntityName, publicCollectionName, dataManagementEnabled, dataManagementStagingTable, entityCategory, allowRowVersionChangeTracking, allowRetention.");
|
|
1558
|
+
((IMetaDataEntityViewProvider)_provider.DataEntityViews).Update(obj, msi);
|
|
1559
|
+
return new { success = true, operation = "modify-property", objectType, objectName, propertyPath, propertyValue, api = "Update" };
|
|
1560
|
+
}
|
|
1551
1561
|
case "menu-item-action":
|
|
1552
1562
|
{
|
|
1553
1563
|
var obj = _provider.MenuItemActions.Read(objectName)
|
|
@@ -2642,6 +2652,16 @@ namespace D365MetadataBridge.Services
|
|
|
2642
2652
|
case "savedatapercompany":
|
|
2643
2653
|
tbl.SaveDataPerCompany = ParseNoYes(value);
|
|
2644
2654
|
break;
|
|
2655
|
+
// Dual-write's table-side change-tracking prerequisite.
|
|
2656
|
+
case "allowrowversionchangetracking":
|
|
2657
|
+
tbl.AllowRowVersionChangeTracking = ParseNoYes(value);
|
|
2658
|
+
break;
|
|
2659
|
+
case "createdby": tbl.CreatedBy = ParseNoYes(value); break;
|
|
2660
|
+
case "createddatetime": tbl.CreatedDateTime = ParseNoYes(value); break;
|
|
2661
|
+
case "createdtransactionid": tbl.CreatedTransactionId = ParseNoYes(value); break;
|
|
2662
|
+
case "modifiedby": tbl.ModifiedBy = ParseNoYes(value); break;
|
|
2663
|
+
case "modifieddatetime": tbl.ModifiedDateTime = ParseNoYes(value); break;
|
|
2664
|
+
case "modifiedtransactionid": tbl.ModifiedTransactionId = ParseNoYes(value); break;
|
|
2645
2665
|
case "tabletype":
|
|
2646
2666
|
if (!Enum.TryParse<Microsoft.Dynamics.AX.Metadata.Core.MetaModel.TableType>(value, true, out var tt))
|
|
2647
2667
|
throw new ArgumentException(
|
|
@@ -2725,6 +2745,39 @@ namespace D365MetadataBridge.Services
|
|
|
2725
2745
|
return true;
|
|
2726
2746
|
}
|
|
2727
2747
|
|
|
2748
|
+
/// <summary>
|
|
2749
|
+
/// AxDataEntityView property setter. AllowRowVersionChangeTracking is
|
|
2750
|
+
/// dual-write's change-tracking switch and is also required on every
|
|
2751
|
+
/// source table.
|
|
2752
|
+
/// </summary>
|
|
2753
|
+
private bool SetAxDataEntityViewProperty(AxDataEntityView e, string prop, string value)
|
|
2754
|
+
{
|
|
2755
|
+
switch (prop.ToLowerInvariant())
|
|
2756
|
+
{
|
|
2757
|
+
case "label": e.Label = value; break;
|
|
2758
|
+
case "developerdocumentation": e.DeveloperDocumentation = value; break;
|
|
2759
|
+
case "primarykey": e.PrimaryKey = value; break;
|
|
2760
|
+
case "publicentityname": e.PublicEntityName = value; break;
|
|
2761
|
+
case "publiccollectionname": e.PublicCollectionName = value; break;
|
|
2762
|
+
case "datamanagementstagingtable": e.DataManagementStagingTable = value; break;
|
|
2763
|
+
case "ispublic": e.IsPublic = ParseNoYes(value); break;
|
|
2764
|
+
case "datamanagementenabled": e.DataManagementEnabled = ParseNoYes(value); break;
|
|
2765
|
+
case "allowrowversionchangetracking": e.AllowRowVersionChangeTracking = ParseNoYes(value); break;
|
|
2766
|
+
case "allowretention": e.AllowRetention = ParseNoYes(value); break;
|
|
2767
|
+
case "entitycategory":
|
|
2768
|
+
if (!Enum.TryParse<Microsoft.Dynamics.AX.Metadata.Core.MetaModel.EntityCategory>(value, true, out var ec))
|
|
2769
|
+
throw new ArgumentException(
|
|
2770
|
+
$"'{value}' is not a valid EntityCategory. Valid values: " +
|
|
2771
|
+
string.Join(", ", Enum.GetNames(typeof(Microsoft.Dynamics.AX.Metadata.Core.MetaModel.EntityCategory))) + ".");
|
|
2772
|
+
e.EntityCategory = ec;
|
|
2773
|
+
break;
|
|
2774
|
+
default:
|
|
2775
|
+
Console.Error.WriteLine($"[WriteService] Unknown AxDataEntityView property: {prop}");
|
|
2776
|
+
return false;
|
|
2777
|
+
}
|
|
2778
|
+
return true;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2728
2781
|
private bool SetAxViewProperty(AxView v, string prop, string value)
|
|
2729
2782
|
{
|
|
2730
2783
|
switch (prop.ToLowerInvariant())
|
|
@@ -1077,7 +1077,7 @@ const BRIDGE_MODIFY_OPS = new Set([
|
|
|
1077
1077
|
*/
|
|
1078
1078
|
const BRIDGE_MODIFY_TYPES = new Set([
|
|
1079
1079
|
'class', 'table', 'enum', 'edt',
|
|
1080
|
-
'form', 'query', 'view',
|
|
1080
|
+
'form', 'query', 'view', 'data-entity',
|
|
1081
1081
|
'class-extension', 'table-extension', 'form-extension', 'enum-extension',
|
|
1082
1082
|
'menu-item-action', 'menu-item-display', 'menu-item-output',
|
|
1083
1083
|
'menu',
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model descriptor reader — `<PackagesLocalDirectory>/<Package>/Descriptor/<Model>.xml`.
|
|
3
|
+
*
|
|
4
|
+
* `<ModuleReferences>` is the only statement of what a model may see: xppc
|
|
5
|
+
* resolves types against the referenced packages, not against everything
|
|
6
|
+
* installed, so an indexed type can still be invisible to the model being
|
|
7
|
+
* compiled. The visible set is the model's own package plus its DIRECT
|
|
8
|
+
* references — walking the closure would mark such a type visible again and
|
|
9
|
+
* hide the defect this exists to catch.
|
|
10
|
+
*
|
|
11
|
+
* Not a compiler: a table in a referenced package can still need a further
|
|
12
|
+
* reference because a third package contributes a table extension to it.
|
|
13
|
+
*
|
|
14
|
+
* build_d365fo_project reads the same element to order its build queue and
|
|
15
|
+
* calls straight into `parseModuleReferences`.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Extract every `<d2p1:string>` entry of a descriptor's `<ModuleReferences>`.
|
|
19
|
+
* The sibling `<ModelReferences>` is `i:nil` in every descriptor observed on a
|
|
20
|
+
* real box, so the flat scan cannot pick up entries from it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseModuleReferences(descriptorXml: string): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Read a model's direct module references, or null when it has no readable
|
|
25
|
+
* descriptor — which callers must treat as unknown, never as "references nothing".
|
|
26
|
+
*/
|
|
27
|
+
export declare function readModuleReferences(packagesPath: string, modelName: string): Promise<string[] | null>;
|
|
28
|
+
/** The PackagesLocalDirectory root contained in a package or workspace path. */
|
|
29
|
+
export declare function packagesRootFromPath(candidate: string | undefined | null): string | null;
|
|
30
|
+
export interface ModelVisibility {
|
|
31
|
+
/** Target model, for diagnostics. */
|
|
32
|
+
model: string;
|
|
33
|
+
/** PackagesLocalDirectory root the indexed file paths are relative to. */
|
|
34
|
+
packagesRoot: string;
|
|
35
|
+
/** Lower-cased package folder names the model may reference (incl. its own). */
|
|
36
|
+
visiblePackages: ReadonlySet<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Package folder owning an indexed file, or null when the path is not under
|
|
39
|
+
* this packages root. Null means "cannot tell" and must silence the check.
|
|
40
|
+
*/
|
|
41
|
+
packageOf(filePath: string): string | null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build (and memoise) the visibility oracle for `modelName`; null whenever the
|
|
45
|
+
* answer would be a guess. Callers must then skip the check — a missing
|
|
46
|
+
* descriptor turning into a wall of errors is worse than the gap it closes.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getModelVisibility(packagesRoot: string | null | undefined, modelName: string | null | undefined): ModelVisibility | null;
|
|
49
|
+
/** Uncached form — exported for tests, which need a fresh fixture each time. */
|
|
50
|
+
export declare function buildModelVisibility(packagesRoot: string | null | undefined, modelName: string | null | undefined): ModelVisibility | null;
|
|
51
|
+
/** Test seam — drops the memoised visibility oracles. */
|
|
52
|
+
export declare function clearModelVisibilityCache(): void;
|
|
53
|
+
//# sourceMappingURL=modelDescriptor.d.ts.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model descriptor reader — `<PackagesLocalDirectory>/<Package>/Descriptor/<Model>.xml`.
|
|
3
|
+
*
|
|
4
|
+
* `<ModuleReferences>` is the only statement of what a model may see: xppc
|
|
5
|
+
* resolves types against the referenced packages, not against everything
|
|
6
|
+
* installed, so an indexed type can still be invisible to the model being
|
|
7
|
+
* compiled. The visible set is the model's own package plus its DIRECT
|
|
8
|
+
* references — walking the closure would mark such a type visible again and
|
|
9
|
+
* hide the defect this exists to catch.
|
|
10
|
+
*
|
|
11
|
+
* Not a compiler: a table in a referenced package can still need a further
|
|
12
|
+
* reference because a third package contributes a table extension to it.
|
|
13
|
+
*
|
|
14
|
+
* build_d365fo_project reads the same element to order its build queue and
|
|
15
|
+
* calls straight into `parseModuleReferences`.
|
|
16
|
+
*/
|
|
17
|
+
import { readFile } from 'fs/promises';
|
|
18
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
/**
|
|
21
|
+
* Extract every `<d2p1:string>` entry of a descriptor's `<ModuleReferences>`.
|
|
22
|
+
* The sibling `<ModelReferences>` is `i:nil` in every descriptor observed on a
|
|
23
|
+
* real box, so the flat scan cannot pick up entries from it.
|
|
24
|
+
*/
|
|
25
|
+
export function parseModuleReferences(descriptorXml) {
|
|
26
|
+
return Array.from(descriptorXml.matchAll(/<d2p1:string>\s*([^<\s]+)\s*<\/d2p1:string>/g))
|
|
27
|
+
.map(m => m[1].trim())
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read a model's direct module references, or null when it has no readable
|
|
32
|
+
* descriptor — which callers must treat as unknown, never as "references nothing".
|
|
33
|
+
*/
|
|
34
|
+
export async function readModuleReferences(packagesPath, modelName) {
|
|
35
|
+
try {
|
|
36
|
+
return parseModuleReferences(await readFile(path.join(packagesPath, modelName, 'Descriptor', `${modelName}.xml`), 'utf-8'));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** The PackagesLocalDirectory root contained in a package or workspace path. */
|
|
43
|
+
export function packagesRootFromPath(candidate) {
|
|
44
|
+
if (!candidate)
|
|
45
|
+
return null;
|
|
46
|
+
const m = /^(.+[\\/]PackagesLocalDirectory)(?:[\\/]|$)/i.exec(candidate.replace(/\//g, '\\'));
|
|
47
|
+
return m ? m[1] : null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Locate `<Model>.xml`. `<root>/<Model>/Descriptor/<Model>.xml` is the common
|
|
51
|
+
* layout and costs one stat; ISV models inside a differently-named package fall
|
|
52
|
+
* back to a single bounded sweep of the root's package folders.
|
|
53
|
+
*/
|
|
54
|
+
function findDescriptor(packagesRoot, modelName) {
|
|
55
|
+
const direct = path.join(packagesRoot, modelName, 'Descriptor', `${modelName}.xml`);
|
|
56
|
+
if (existsSync(direct))
|
|
57
|
+
return { file: direct, pkg: modelName };
|
|
58
|
+
let entries;
|
|
59
|
+
try {
|
|
60
|
+
entries = readdirSync(packagesRoot);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
for (const pkg of entries) {
|
|
66
|
+
const file = path.join(packagesRoot, pkg, 'Descriptor', `${modelName}.xml`);
|
|
67
|
+
if (existsSync(file))
|
|
68
|
+
return { file, pkg };
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/** Cache key → resolved visibility (or null when it could not be resolved). */
|
|
73
|
+
const visibilityCache = new Map();
|
|
74
|
+
/**
|
|
75
|
+
* Build (and memoise) the visibility oracle for `modelName`; null whenever the
|
|
76
|
+
* answer would be a guess. Callers must then skip the check — a missing
|
|
77
|
+
* descriptor turning into a wall of errors is worse than the gap it closes.
|
|
78
|
+
*/
|
|
79
|
+
export function getModelVisibility(packagesRoot, modelName) {
|
|
80
|
+
if (!packagesRoot || !modelName)
|
|
81
|
+
return null;
|
|
82
|
+
const key = `${packagesRoot.toLowerCase()}|${modelName.toLowerCase()}`;
|
|
83
|
+
const cached = visibilityCache.get(key);
|
|
84
|
+
if (cached !== undefined)
|
|
85
|
+
return cached;
|
|
86
|
+
const built = buildModelVisibility(packagesRoot, modelName);
|
|
87
|
+
visibilityCache.set(key, built);
|
|
88
|
+
return built;
|
|
89
|
+
}
|
|
90
|
+
/** Uncached form — exported for tests, which need a fresh fixture each time. */
|
|
91
|
+
export function buildModelVisibility(packagesRoot, modelName) {
|
|
92
|
+
if (!packagesRoot || !modelName)
|
|
93
|
+
return null;
|
|
94
|
+
const found = findDescriptor(packagesRoot, modelName);
|
|
95
|
+
if (!found)
|
|
96
|
+
return null;
|
|
97
|
+
let refs;
|
|
98
|
+
try {
|
|
99
|
+
refs = parseModuleReferences(readFileSync(found.file, 'utf-8'));
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const visiblePackages = new Set([found.pkg.toLowerCase()]);
|
|
105
|
+
for (const ref of refs)
|
|
106
|
+
visiblePackages.add(ref.toLowerCase());
|
|
107
|
+
const rootPrefix = packagesRoot.replace(/\//g, '\\').replace(/\\+$/, '').toLowerCase() + '\\';
|
|
108
|
+
return {
|
|
109
|
+
model: modelName,
|
|
110
|
+
packagesRoot,
|
|
111
|
+
visiblePackages,
|
|
112
|
+
packageOf(filePath) {
|
|
113
|
+
if (!filePath)
|
|
114
|
+
return null;
|
|
115
|
+
const norm = filePath.replace(/\//g, '\\');
|
|
116
|
+
// Case-insensitive: index and config disagree on the root's casing
|
|
117
|
+
// (`K:\AOSService\…` vs `K:\AosService\…`) for the same directory.
|
|
118
|
+
if (!norm.toLowerCase().startsWith(rootPrefix))
|
|
119
|
+
return null;
|
|
120
|
+
const segment = norm.slice(rootPrefix.length).split('\\')[0];
|
|
121
|
+
return segment || null;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** Test seam — drops the memoised visibility oracles. */
|
|
126
|
+
export function clearModelVisibilityCache() {
|
|
127
|
+
visibilityCache.clear();
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=modelDescriptor.js.map
|
|
@@ -6,6 +6,7 @@ import Database from '../database/sqlite.js';
|
|
|
6
6
|
import { Worker } from 'node:worker_threads';
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
+
import { renderMethodSignature } from './xppDeclaration.js';
|
|
9
10
|
import { isStandardModel } from '../utils/modelClassifier.js';
|
|
10
11
|
import { c, log } from '../utils/terminalUi.js';
|
|
11
12
|
/**
|
|
@@ -1531,12 +1532,11 @@ export class XppSymbolIndex {
|
|
|
1531
1532
|
// Add method symbols with enhanced metadata
|
|
1532
1533
|
if (classData.methods && Array.isArray(classData.methods)) {
|
|
1533
1534
|
for (const method of classData.methods) {
|
|
1534
|
-
const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(', ') || '';
|
|
1535
1535
|
this.addSymbol({
|
|
1536
1536
|
name: method.name,
|
|
1537
1537
|
type: 'method',
|
|
1538
1538
|
parentName: classData.name,
|
|
1539
|
-
signature:
|
|
1539
|
+
signature: renderMethodSignature(method),
|
|
1540
1540
|
filePath: sourceFilePath,
|
|
1541
1541
|
model,
|
|
1542
1542
|
description: method.documentation,
|
|
@@ -1595,12 +1595,11 @@ export class XppSymbolIndex {
|
|
|
1595
1595
|
// Add method symbols (parallel to indexClasses)
|
|
1596
1596
|
if (tableData.methods && Array.isArray(tableData.methods)) {
|
|
1597
1597
|
for (const method of tableData.methods) {
|
|
1598
|
-
const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(', ') || '';
|
|
1599
1598
|
this.addSymbol({
|
|
1600
1599
|
name: method.name,
|
|
1601
1600
|
type: 'method',
|
|
1602
1601
|
parentName: tableData.name,
|
|
1603
|
-
signature:
|
|
1602
|
+
signature: renderMethodSignature(method),
|
|
1604
1603
|
filePath: sourceFilePath,
|
|
1605
1604
|
model,
|
|
1606
1605
|
description: method.documentation,
|
|
@@ -1848,12 +1847,11 @@ export class XppSymbolIndex {
|
|
|
1848
1847
|
// Add method symbols (views and data-entities can have display/computed methods)
|
|
1849
1848
|
if (viewData.methods && Array.isArray(viewData.methods)) {
|
|
1850
1849
|
for (const method of viewData.methods) {
|
|
1851
|
-
const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(', ') || '';
|
|
1852
1850
|
this.addSymbol({
|
|
1853
1851
|
name: method.name,
|
|
1854
1852
|
type: 'method',
|
|
1855
1853
|
parentName: viewName,
|
|
1856
|
-
signature:
|
|
1854
|
+
signature: renderMethodSignature(method),
|
|
1857
1855
|
filePath: sourceFilePath,
|
|
1858
1856
|
model,
|
|
1859
1857
|
description: method.documentation,
|
package/dist/metadata/types.d.ts
CHANGED
|
@@ -34,6 +34,12 @@ export interface XppMethodInfo {
|
|
|
34
34
|
visibility: 'public' | 'private' | 'protected';
|
|
35
35
|
returnType: string;
|
|
36
36
|
parameters: XppParameterInfo[];
|
|
37
|
+
/**
|
|
38
|
+
* Declaration unparseable, so `parameters` is empty for lack of evidence
|
|
39
|
+
* rather than because the method takes none. Arity checks must skip, not
|
|
40
|
+
* assume zero.
|
|
41
|
+
*/
|
|
42
|
+
parametersUnknown?: boolean;
|
|
37
43
|
isStatic: boolean;
|
|
38
44
|
source: string;
|
|
39
45
|
documentation?: string;
|
|
@@ -48,6 +54,8 @@ export interface XppMethodInfo {
|
|
|
48
54
|
export interface XppParameterInfo {
|
|
49
55
|
name: string;
|
|
50
56
|
type: string;
|
|
57
|
+
/** Default as declared; a dropped default makes an optional parameter look required. */
|
|
58
|
+
defaultValue?: string;
|
|
51
59
|
}
|
|
52
60
|
export interface XppTableInfo {
|
|
53
61
|
name: string;
|
|
@@ -93,7 +93,11 @@ export declare class XppMetadataParser {
|
|
|
93
93
|
private parseViewRelationFields;
|
|
94
94
|
private extractLabelId;
|
|
95
95
|
private ensureArray;
|
|
96
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Declaration parameters narrowed to the shape XppMethodInfo carries.
|
|
98
|
+
* A null decl yields `[]`; the `parametersUnknown` flag set alongside is what
|
|
99
|
+
* tells that apart from a genuinely empty list, so don't read this alone.
|
|
100
|
+
*/
|
|
97
101
|
private toParameterInfo;
|
|
98
102
|
/**
|
|
99
103
|
* Parse Form XML file (AxForm)
|
|
@@ -262,6 +262,7 @@ export class XppMetadataParser {
|
|
|
262
262
|
visibility: this.parseVisibility(method.Visibility),
|
|
263
263
|
returnType: decl?.returnType || method.ReturnType || 'void',
|
|
264
264
|
parameters: this.toParameterInfo(decl),
|
|
265
|
+
parametersUnknown: decl === null,
|
|
265
266
|
isStatic: decl?.modifiers.includes('static') ?? false,
|
|
266
267
|
source: source,
|
|
267
268
|
documentation: method.DeveloperDocumentation || undefined,
|
|
@@ -441,9 +442,15 @@ export class XppMetadataParser {
|
|
|
441
442
|
return [];
|
|
442
443
|
return Array.isArray(value) ? value : [value];
|
|
443
444
|
}
|
|
444
|
-
/**
|
|
445
|
+
/**
|
|
446
|
+
* Declaration parameters narrowed to the shape XppMethodInfo carries.
|
|
447
|
+
* A null decl yields `[]`; the `parametersUnknown` flag set alongside is what
|
|
448
|
+
* tells that apart from a genuinely empty list, so don't read this alone.
|
|
449
|
+
*/
|
|
445
450
|
toParameterInfo(decl) {
|
|
446
|
-
return decl?.parameters.map(p => (
|
|
451
|
+
return decl?.parameters.map(p => (p.defaultValue
|
|
452
|
+
? { type: p.type, name: p.name, defaultValue: p.defaultValue }
|
|
453
|
+
: { type: p.type, name: p.name })) ?? [];
|
|
447
454
|
}
|
|
448
455
|
/**
|
|
449
456
|
* Parse Form XML file (AxForm)
|
|
@@ -561,6 +568,7 @@ export class XppMetadataParser {
|
|
|
561
568
|
visibility: 'public', // Forms typically have public methods
|
|
562
569
|
returnType: decl?.returnType || 'void',
|
|
563
570
|
parameters: this.toParameterInfo(decl),
|
|
571
|
+
parametersUnknown: decl === null,
|
|
564
572
|
isStatic: decl?.modifiers.includes('static') ?? false,
|
|
565
573
|
source,
|
|
566
574
|
sourceSnippet: source.split('\n').slice(0, 10).join('\n'),
|
|
@@ -26,6 +26,28 @@ export interface XppDeclaration {
|
|
|
26
26
|
returnType: string;
|
|
27
27
|
parameters: XppDeclarationParameter[];
|
|
28
28
|
}
|
|
29
|
+
/** Rendered for a parameter list that could not be read; not legal X++, so it can't collide with a real one. */
|
|
30
|
+
export declare const UNKNOWN_PARAMETER_LIST = "...";
|
|
31
|
+
/** The subset of a method that `renderMethodSignature` needs. */
|
|
32
|
+
export interface RenderableMethod {
|
|
33
|
+
name: string;
|
|
34
|
+
returnType?: string;
|
|
35
|
+
parameters?: ReadonlyArray<{
|
|
36
|
+
type: string;
|
|
37
|
+
name: string;
|
|
38
|
+
defaultValue?: string;
|
|
39
|
+
}> | null;
|
|
40
|
+
/** See XppMethodInfo.parametersUnknown. */
|
|
41
|
+
parametersUnknown?: boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Render `ReturnType name(Type _a, Type _b = default)` for the symbol index.
|
|
45
|
+
*
|
|
46
|
+
* Defaults are kept and an unreadable list renders as `(...)`, never `()` —
|
|
47
|
+
* resolve_references reads this string back and gates writes on it, so `()` is
|
|
48
|
+
* a positive claim of zero parameters that consumers act on.
|
|
49
|
+
*/
|
|
50
|
+
export declare function renderMethodSignature(method: RenderableMethod): string;
|
|
29
51
|
export interface XppClassHeader {
|
|
30
52
|
kind: 'class' | 'interface';
|
|
31
53
|
name: string;
|
|
@@ -15,6 +15,23 @@
|
|
|
15
15
|
* A parse that cannot be trusted returns null so callers fall through to their
|
|
16
16
|
* own fallbacks, rather than emitting a confidently wrong signature.
|
|
17
17
|
*/
|
|
18
|
+
/** Rendered for a parameter list that could not be read; not legal X++, so it can't collide with a real one. */
|
|
19
|
+
export const UNKNOWN_PARAMETER_LIST = '...';
|
|
20
|
+
/**
|
|
21
|
+
* Render `ReturnType name(Type _a, Type _b = default)` for the symbol index.
|
|
22
|
+
*
|
|
23
|
+
* Defaults are kept and an unreadable list renders as `(...)`, never `()` —
|
|
24
|
+
* resolve_references reads this string back and gates writes on it, so `()` is
|
|
25
|
+
* a positive claim of zero parameters that consumers act on.
|
|
26
|
+
*/
|
|
27
|
+
export function renderMethodSignature(method) {
|
|
28
|
+
const params = method.parametersUnknown
|
|
29
|
+
? UNKNOWN_PARAMETER_LIST
|
|
30
|
+
: (method.parameters ?? [])
|
|
31
|
+
.map(p => `${p.type} ${p.name}${p.defaultValue ? ` = ${p.defaultValue}` : ''}`)
|
|
32
|
+
.join(', ');
|
|
33
|
+
return `${method.returnType ?? 'void'} ${method.name}(${params})`;
|
|
34
|
+
}
|
|
18
35
|
export const MODIFIER_KEYWORDS = [
|
|
19
36
|
'public', 'private', 'protected', 'internal', 'static', 'final', 'abstract', 'display', 'edit',
|
|
20
37
|
];
|
|
@@ -126,6 +143,20 @@ function isTypeToken(t) {
|
|
|
126
143
|
function isNameToken(t) {
|
|
127
144
|
return /^[A-Za-z_]\w*$/.test(t);
|
|
128
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Slice `raw` from `from`, trimmed to the extent that still carries content in
|
|
148
|
+
* the blanked twin — comments became whitespace there and so drop out, while
|
|
149
|
+
* string literals survive (their quotes are not blanked).
|
|
150
|
+
*/
|
|
151
|
+
function sliceByBlankedExtent(raw, blanked, from) {
|
|
152
|
+
let start = from;
|
|
153
|
+
while (start < blanked.length && /\s/.test(blanked[start]))
|
|
154
|
+
start++;
|
|
155
|
+
let end = blanked.length;
|
|
156
|
+
while (end > start && /\s/.test(blanked[end - 1]))
|
|
157
|
+
end--;
|
|
158
|
+
return raw.slice(start, end).replace(/\s+/g, ' ');
|
|
159
|
+
}
|
|
129
160
|
/**
|
|
130
161
|
* Parse one "Type _name [= default]" parameter; null when it doesn't look like
|
|
131
162
|
* a declared parameter (which is how a call's arguments are told apart from a
|
|
@@ -146,8 +177,11 @@ function parseParameter(raw, blanked) {
|
|
|
146
177
|
break;
|
|
147
178
|
}
|
|
148
179
|
}
|
|
149
|
-
|
|
150
|
-
|
|
180
|
+
// Type and name come off the blanked twin: the standard models interleave
|
|
181
|
+
// pragma comments inside wrapped parameter lists, and read raw those look
|
|
182
|
+
// like a leading type token, failing the parameter and discarding the lot.
|
|
183
|
+
const left = (eq >= 0 ? blanked.slice(0, eq) : blanked).trim().replace(/\s+/g, ' ');
|
|
184
|
+
const defaultValue = eq >= 0 ? sliceByBlankedExtent(raw, blanked, eq + 1) : undefined;
|
|
151
185
|
const tokens = left.split(' ').filter(Boolean);
|
|
152
186
|
if (tokens.length < 2)
|
|
153
187
|
return null;
|
|
@@ -785,6 +785,13 @@ import { Worker } from "node:worker_threads";
|
|
|
785
785
|
import * as fs2 from "fs";
|
|
786
786
|
import * as path from "path";
|
|
787
787
|
|
|
788
|
+
// src/metadata/xppDeclaration.ts
|
|
789
|
+
var UNKNOWN_PARAMETER_LIST = "...";
|
|
790
|
+
function renderMethodSignature(method) {
|
|
791
|
+
const params = method.parametersUnknown ? UNKNOWN_PARAMETER_LIST : (method.parameters ?? []).map((p) => `${p.type} ${p.name}${p.defaultValue ? ` = ${p.defaultValue}` : ""}`).join(", ");
|
|
792
|
+
return `${method.returnType ?? "void"} ${method.name}(${params})`;
|
|
793
|
+
}
|
|
794
|
+
|
|
788
795
|
// src/utils/modelClassifier.ts
|
|
789
796
|
var autoDetectedCustomModels = /* @__PURE__ */ new Set();
|
|
790
797
|
function isAutoDetectedCustomModel(modelName) {
|
|
@@ -2342,12 +2349,11 @@ Point the installation at a drive with room (a full index needs several GB): re-
|
|
|
2342
2349
|
});
|
|
2343
2350
|
if (classData.methods && Array.isArray(classData.methods)) {
|
|
2344
2351
|
for (const method of classData.methods) {
|
|
2345
|
-
const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(", ") || "";
|
|
2346
2352
|
this.addSymbol({
|
|
2347
2353
|
name: method.name,
|
|
2348
2354
|
type: "method",
|
|
2349
2355
|
parentName: classData.name,
|
|
2350
|
-
signature:
|
|
2356
|
+
signature: renderMethodSignature(method),
|
|
2351
2357
|
filePath: sourceFilePath,
|
|
2352
2358
|
model,
|
|
2353
2359
|
description: method.documentation,
|
|
@@ -2401,12 +2407,11 @@ Point the installation at a drive with room (a full index needs several GB): re-
|
|
|
2401
2407
|
}
|
|
2402
2408
|
if (tableData.methods && Array.isArray(tableData.methods)) {
|
|
2403
2409
|
for (const method of tableData.methods) {
|
|
2404
|
-
const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(", ") || "";
|
|
2405
2410
|
this.addSymbol({
|
|
2406
2411
|
name: method.name,
|
|
2407
2412
|
type: "method",
|
|
2408
2413
|
parentName: tableData.name,
|
|
2409
|
-
signature:
|
|
2414
|
+
signature: renderMethodSignature(method),
|
|
2410
2415
|
filePath: sourceFilePath,
|
|
2411
2416
|
model,
|
|
2412
2417
|
description: method.documentation,
|
|
@@ -2659,12 +2664,11 @@ Point the installation at a drive with room (a full index needs several GB): re-
|
|
|
2659
2664
|
}
|
|
2660
2665
|
if (viewData.methods && Array.isArray(viewData.methods)) {
|
|
2661
2666
|
for (const method of viewData.methods) {
|
|
2662
|
-
const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(", ") || "";
|
|
2663
2667
|
this.addSymbol({
|
|
2664
2668
|
name: method.name,
|
|
2665
2669
|
type: "method",
|
|
2666
2670
|
parentName: viewName,
|
|
2667
|
-
signature:
|
|
2671
|
+
signature: renderMethodSignature(method),
|
|
2668
2672
|
filePath: sourceFilePath,
|
|
2669
2673
|
model,
|
|
2670
2674
|
description: method.documentation,
|
|
@@ -784,6 +784,13 @@ import { Worker } from "node:worker_threads";
|
|
|
784
784
|
import * as fs2 from "fs";
|
|
785
785
|
import * as path from "path";
|
|
786
786
|
|
|
787
|
+
// src/metadata/xppDeclaration.ts
|
|
788
|
+
var UNKNOWN_PARAMETER_LIST = "...";
|
|
789
|
+
function renderMethodSignature(method) {
|
|
790
|
+
const params = method.parametersUnknown ? UNKNOWN_PARAMETER_LIST : (method.parameters ?? []).map((p) => `${p.type} ${p.name}${p.defaultValue ? ` = ${p.defaultValue}` : ""}`).join(", ");
|
|
791
|
+
return `${method.returnType ?? "void"} ${method.name}(${params})`;
|
|
792
|
+
}
|
|
793
|
+
|
|
787
794
|
// src/utils/modelClassifier.ts
|
|
788
795
|
var autoDetectedCustomModels = /* @__PURE__ */ new Set();
|
|
789
796
|
function isAutoDetectedCustomModel(modelName) {
|
|
@@ -2281,12 +2288,11 @@ Point the installation at a drive with room (a full index needs several GB): re-
|
|
|
2281
2288
|
});
|
|
2282
2289
|
if (classData.methods && Array.isArray(classData.methods)) {
|
|
2283
2290
|
for (const method of classData.methods) {
|
|
2284
|
-
const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(", ") || "";
|
|
2285
2291
|
this.addSymbol({
|
|
2286
2292
|
name: method.name,
|
|
2287
2293
|
type: "method",
|
|
2288
2294
|
parentName: classData.name,
|
|
2289
|
-
signature:
|
|
2295
|
+
signature: renderMethodSignature(method),
|
|
2290
2296
|
filePath: sourceFilePath,
|
|
2291
2297
|
model,
|
|
2292
2298
|
description: method.documentation,
|
|
@@ -2340,12 +2346,11 @@ Point the installation at a drive with room (a full index needs several GB): re-
|
|
|
2340
2346
|
}
|
|
2341
2347
|
if (tableData.methods && Array.isArray(tableData.methods)) {
|
|
2342
2348
|
for (const method of tableData.methods) {
|
|
2343
|
-
const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(", ") || "";
|
|
2344
2349
|
this.addSymbol({
|
|
2345
2350
|
name: method.name,
|
|
2346
2351
|
type: "method",
|
|
2347
2352
|
parentName: tableData.name,
|
|
2348
|
-
signature:
|
|
2353
|
+
signature: renderMethodSignature(method),
|
|
2349
2354
|
filePath: sourceFilePath,
|
|
2350
2355
|
model,
|
|
2351
2356
|
description: method.documentation,
|
|
@@ -2598,12 +2603,11 @@ Point the installation at a drive with room (a full index needs several GB): re-
|
|
|
2598
2603
|
}
|
|
2599
2604
|
if (viewData.methods && Array.isArray(viewData.methods)) {
|
|
2600
2605
|
for (const method of viewData.methods) {
|
|
2601
|
-
const params = method.parameters?.map((p) => `${p.type} ${p.name}`).join(", ") || "";
|
|
2602
2606
|
this.addSymbol({
|
|
2603
2607
|
name: method.name,
|
|
2604
2608
|
type: "method",
|
|
2605
2609
|
parentName: viewName,
|
|
2606
|
-
signature:
|
|
2610
|
+
signature: renderMethodSignature(method),
|
|
2607
2611
|
filePath: sourceFilePath,
|
|
2608
2612
|
model,
|
|
2609
2613
|
description: method.documentation,
|