arc-1 0.6.10 → 0.7.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/README.md +8 -7
- package/bin/arc1-cli.js +10 -0
- package/bin/arc1.js +1 -1
- package/dist/adt/cds-impact.d.ts +35 -0
- package/dist/adt/cds-impact.d.ts.map +1 -1
- package/dist/adt/cds-impact.js +71 -0
- package/dist/adt/cds-impact.js.map +1 -1
- package/dist/adt/client.d.ts +4 -1
- package/dist/adt/client.d.ts.map +1 -1
- package/dist/adt/client.js +18 -5
- package/dist/adt/client.js.map +1 -1
- package/dist/adt/crud.d.ts.map +1 -1
- package/dist/adt/crud.js +32 -5
- package/dist/adt/crud.js.map +1 -1
- package/dist/adt/devtools.d.ts +39 -3
- package/dist/adt/devtools.d.ts.map +1 -1
- package/dist/adt/devtools.js +237 -25
- package/dist/adt/devtools.js.map +1 -1
- package/dist/adt/diagnostics.d.ts +69 -7
- package/dist/adt/diagnostics.d.ts.map +1 -1
- package/dist/adt/diagnostics.js +694 -36
- package/dist/adt/diagnostics.js.map +1 -1
- package/dist/adt/errors.d.ts +14 -1
- package/dist/adt/errors.d.ts.map +1 -1
- package/dist/adt/errors.js +40 -9
- package/dist/adt/errors.js.map +1 -1
- package/dist/adt/http.d.ts.map +1 -1
- package/dist/adt/http.js +86 -1
- package/dist/adt/http.js.map +1 -1
- package/dist/adt/rap-handlers.d.ts +165 -0
- package/dist/adt/rap-handlers.d.ts.map +1 -0
- package/dist/adt/rap-handlers.js +835 -0
- package/dist/adt/rap-handlers.js.map +1 -0
- package/dist/adt/rap-preflight.d.ts +43 -0
- package/dist/adt/rap-preflight.d.ts.map +1 -0
- package/dist/adt/rap-preflight.js +405 -0
- package/dist/adt/rap-preflight.js.map +1 -0
- package/dist/adt/safety.d.ts +60 -36
- package/dist/adt/safety.d.ts.map +1 -1
- package/dist/adt/safety.js +202 -120
- package/dist/adt/safety.js.map +1 -1
- package/dist/adt/transport.d.ts +1 -1
- package/dist/adt/transport.js +2 -2
- package/dist/adt/transport.js.map +1 -1
- package/dist/adt/types.d.ts +88 -0
- package/dist/adt/types.d.ts.map +1 -1
- package/dist/adt/xml-parser.d.ts +13 -1
- package/dist/adt/xml-parser.d.ts.map +1 -1
- package/dist/adt/xml-parser.js +26 -15
- package/dist/adt/xml-parser.js.map +1 -1
- package/dist/authz/policy.d.ts +53 -0
- package/dist/authz/policy.d.ts.map +1 -0
- package/dist/authz/policy.js +199 -0
- package/dist/authz/policy.js.map +1 -0
- package/dist/cli-args.d.ts +14 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +62 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli.d.ts +13 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +252 -55
- package/dist/cli.js.map +1 -1
- package/dist/extract-sap-cookies.d.ts +24 -0
- package/dist/extract-sap-cookies.d.ts.map +1 -0
- package/dist/extract-sap-cookies.js +317 -0
- package/dist/extract-sap-cookies.js.map +1 -0
- package/dist/handlers/hyperfocused.d.ts +4 -3
- package/dist/handlers/hyperfocused.d.ts.map +1 -1
- package/dist/handlers/hyperfocused.js +25 -16
- package/dist/handlers/hyperfocused.js.map +1 -1
- package/dist/handlers/intent.d.ts +4 -12
- package/dist/handlers/intent.d.ts.map +1 -1
- package/dist/handlers/intent.js +1238 -114
- package/dist/handlers/intent.js.map +1 -1
- package/dist/handlers/schemas.d.ts +38 -10
- package/dist/handlers/schemas.d.ts.map +1 -1
- package/dist/handlers/schemas.js +69 -4
- package/dist/handlers/schemas.js.map +1 -1
- package/dist/handlers/tools.d.ts.map +1 -1
- package/dist/handlers/tools.js +251 -164
- package/dist/handlers/tools.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/server/audit.d.ts +26 -3
- package/dist/server/audit.d.ts.map +1 -1
- package/dist/server/audit.js.map +1 -1
- package/dist/server/config.d.ts +34 -19
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +320 -193
- package/dist/server/config.js.map +1 -1
- package/dist/server/deny-actions.d.ts +31 -0
- package/dist/server/deny-actions.d.ts.map +1 -0
- package/dist/server/deny-actions.js +156 -0
- package/dist/server/deny-actions.js.map +1 -0
- package/dist/server/effective-policy-log.d.ts +27 -0
- package/dist/server/effective-policy-log.d.ts.map +1 -0
- package/dist/server/effective-policy-log.js +103 -0
- package/dist/server/effective-policy-log.js.map +1 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +15 -16
- package/dist/server/http.js.map +1 -1
- package/dist/server/server.d.ts +37 -3
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +231 -30
- package/dist/server/server.js.map +1 -1
- package/dist/server/types.d.ts +29 -13
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js +10 -11
- package/dist/server/types.js.map +1 -1
- package/dist/server/xsuaa.d.ts +1 -2
- package/dist/server/xsuaa.d.ts.map +1 -1
- package/dist/server/xsuaa.js +13 -14
- package/dist/server/xsuaa.js.map +1 -1
- package/package.json +8 -3
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAP behavior-pool handler scaffolding.
|
|
3
|
+
*
|
|
4
|
+
* Context — this module exists because on-prem RAP development has a tight
|
|
5
|
+
* contract between a BDEF (behavior definition source) and its behavior pool
|
|
6
|
+
* (the ABAP global class named in `managed implementation in class ZBP_...`).
|
|
7
|
+
* Every `action` / `determination` / `validation` / `authorization master`
|
|
8
|
+
* declared in the BDEF requires a matching METHOD signature inside a local
|
|
9
|
+
* handler class (`lhc_<alias>`). If any signature is missing, the class will
|
|
10
|
+
* not activate and the error reported by ADT doesn't tell the developer
|
|
11
|
+
* which signatures are missing — they see a generic "behavior pool does not
|
|
12
|
+
* implement the required method for ..." message.
|
|
13
|
+
*
|
|
14
|
+
* The exported helpers cooperate:
|
|
15
|
+
* 1. extractRapHandlerRequirements — parse BDEF → list of required methods
|
|
16
|
+
* 2. findMissingRapHandlerRequirements — diff required vs. present in class
|
|
17
|
+
* 3. applyRapHandlerSignatures — insert the missing METHODS lines
|
|
18
|
+
* 4. applyRapHandlerImplementationStubs — insert empty METHOD stubs
|
|
19
|
+
* 5. applyRapHandlerScaffold — plan multi-include auto-apply
|
|
20
|
+
*
|
|
21
|
+
* The scaffolder writes declarations plus empty implementations only.
|
|
22
|
+
* Business logic remains the developer's responsibility and can be filled
|
|
23
|
+
* with edit_method.
|
|
24
|
+
*
|
|
25
|
+
* Naming convention: handler classes use the prefix `lhc_` (local handler
|
|
26
|
+
* class) followed by the lowercased alias from the BDEF. This is the RAP
|
|
27
|
+
* convention SAP's own templates use (see cl_abap_behavior_handler examples
|
|
28
|
+
* in /DMO/* and the "Create Behavior Implementation Class" wizard in ADT).
|
|
29
|
+
*/
|
|
30
|
+
// AI-maintenance guide:
|
|
31
|
+
// If SAP adds or changes RAP handler syntax, update these parser patterns first,
|
|
32
|
+
// then add one fixture-style test in tests/unit/adt/rap-handlers.test.ts. Keeping
|
|
33
|
+
// the grammar fragments here avoids subtly divergent regexes in detection,
|
|
34
|
+
// missing-checks, and auto-apply.
|
|
35
|
+
const BDEF_DEFINE_BEHAVIOR_RE = /^\s*define\s+behavior\s+for\s+([^\s{]+)(?:\s+alias\s+([A-Za-z_]\w*))?/i;
|
|
36
|
+
const BDEF_ACTION_DECLARATION_RE = /^\s*(?:static\s+)?(?:(?:internal|factory)\s+)*action(?:\s*\([^)]*\))?\s+([A-Za-z_]\w*)\b/i;
|
|
37
|
+
const BDEF_DETERMINATION_DECLARATION_RE = /^\s*determination\s+([A-Za-z_]\w*)\s+on\s+(modify|save)\b/i;
|
|
38
|
+
const BDEF_VALIDATION_DECLARATION_RE = /^\s*validation\s+([A-Za-z_]\w*)\s+on\s+(modify|save)\b/i;
|
|
39
|
+
const BDEF_INSTANCE_AUTH_RE = /\bauthorization\s+master\s*\(\s*instance\s*\)/i;
|
|
40
|
+
const BDEF_GLOBAL_AUTH_RE = /\bauthorization\s+master\s*\(\s*global\s*\)/i;
|
|
41
|
+
const CLASS_DEFINITION_START_RE = /^\s*CLASS\s+([A-Za-z_][\w$]*)\s+DEFINITION\b/i;
|
|
42
|
+
const CLASS_DEFINITION_DEFERRED_RE = /\bDEFINITION\b.*\bDEFERRED\b/i;
|
|
43
|
+
const CLASS_IMPLEMENTATION_START_RE = /^\s*CLASS\s+([A-Za-z_][\w$]*)\s+IMPLEMENTATION\s*\./i;
|
|
44
|
+
const PRIVATE_SECTION_RE = /^\s*PRIVATE\s+SECTION\./i;
|
|
45
|
+
const ENDCLASS_RE = /^\s*ENDCLASS\./i;
|
|
46
|
+
const METHOD_DECLARATION_RE = /^\s*(?:CLASS-)?METHODS\s+([A-Za-z_~][\w~]*)/i;
|
|
47
|
+
const METHOD_IMPLEMENTATION_RE = /^\s*METHOD\s+([A-Za-z_~][\w~]*)\s*\./i;
|
|
48
|
+
const HANDLER_ACTION_BINDING_RE = /\bFOR\s+ACTION\s+([A-Za-z_]\w*)\s*~\s*([A-Za-z_]\w*)/i;
|
|
49
|
+
const HANDLER_ENTITY_BINDING_RE = /\bFOR\s+(?!ACTION\b|MODIFY\b|READ\b|VALIDATE\b|DETERMINE\b|INSTANCE\b|GLOBAL\b)([A-Za-z_]\w*)\s*~\s*([A-Za-z_]\w*)/i;
|
|
50
|
+
const HANDLER_AUTH_BINDING_RE = /\bAUTHORIZATION\b.*\bFOR\s+([A-Za-z_]\w*)\s+RESULT\b/i;
|
|
51
|
+
const HANDLER_DETERMINE_STATEMENT_RE = /\bFOR\s+DETERMINE\s+ON\b/i;
|
|
52
|
+
const HANDLER_VALIDATE_STATEMENT_RE = /\bFOR\s+VALIDATE\s+ON\b/i;
|
|
53
|
+
const HANDLER_INSTANCE_AUTH_STATEMENT_RE = /\bFOR\s+INSTANCE\s+AUTHORIZATION\b/i;
|
|
54
|
+
const HANDLER_GLOBAL_AUTH_STATEMENT_RE = /\bFOR\s+GLOBAL\s+AUTHORIZATION\b/i;
|
|
55
|
+
function bindingKey(targetHandlerClass, kind, methodName, entityAlias) {
|
|
56
|
+
return [targetHandlerClass.toLowerCase(), kind, normalizeMethodName(methodName), entityAlias.toLowerCase()].join('|');
|
|
57
|
+
}
|
|
58
|
+
export function rapHandlerRequirementKey(requirement) {
|
|
59
|
+
return bindingKey(requirement.targetHandlerClass, requirement.kind, requirement.methodName, requirement.entityAlias);
|
|
60
|
+
}
|
|
61
|
+
function countChar(value, char) {
|
|
62
|
+
return value.split(char).length - 1;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Collect a BDEF statement that may span multiple lines.
|
|
66
|
+
*
|
|
67
|
+
* RAP behavior definitions are terminated by `;`, but developers routinely
|
|
68
|
+
* split long declarations across lines for readability, e.g.:
|
|
69
|
+
* action acceptTravel
|
|
70
|
+
* result [1] $self;
|
|
71
|
+
* We must join the continuation lines before deciding whether a `result`
|
|
72
|
+
* clause is present — otherwise we'd emit `FOR ACTION ... RESULT result` for
|
|
73
|
+
* actions that don't return anything, producing an invalid handler signature.
|
|
74
|
+
*
|
|
75
|
+
* The 20-line safety cutoff guards against runaway scans when the BDEF is
|
|
76
|
+
* malformed or truncated; real declarations rarely exceed 5-6 lines.
|
|
77
|
+
*/
|
|
78
|
+
function collectStatement(lines, startIdx) {
|
|
79
|
+
let statement = lines[startIdx] ?? '';
|
|
80
|
+
if (statement.includes(';'))
|
|
81
|
+
return statement;
|
|
82
|
+
for (let j = startIdx + 1; j < lines.length; j += 1) {
|
|
83
|
+
const next = lines[j] ?? '';
|
|
84
|
+
statement += ` ${next}`;
|
|
85
|
+
if (next.includes(';'))
|
|
86
|
+
break;
|
|
87
|
+
if (j - startIdx > 20)
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
return statement;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Collect a multi-line ABAP statement terminated by `.`.
|
|
94
|
+
*
|
|
95
|
+
* Behavior handler declarations are often split across lines:
|
|
96
|
+
* METHODS set_status_accepted FOR MODIFY
|
|
97
|
+
* IMPORTING keys FOR ACTION travel~acceptTravel RESULT result.
|
|
98
|
+
* Binding parsing needs the continuation lines; otherwise semantic method
|
|
99
|
+
* names cannot be mapped back to the BDEF action/determination/validation.
|
|
100
|
+
*/
|
|
101
|
+
function collectAbapStatement(lines, startIdx, endIdx, maxContinuation = 10) {
|
|
102
|
+
let statement = lines[startIdx] ?? '';
|
|
103
|
+
for (let j = startIdx + 1; j <= endIdx && j < startIdx + maxContinuation; j += 1) {
|
|
104
|
+
const cont = lines[j] ?? '';
|
|
105
|
+
statement += ` ${cont}`;
|
|
106
|
+
if (/\.\s*$/.test(cont.trim()))
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
return statement;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Lowercase a BDEF identifier for use as an ABAP METHOD name.
|
|
113
|
+
*
|
|
114
|
+
* BDEF is case-insensitive for identifier matching but ABAP source code is
|
|
115
|
+
* rendered in lowercase by SAP's pretty printer. We emit lowercase here so
|
|
116
|
+
* the scaffolded METHODS lines match both the `lhc_<alias>` class name and
|
|
117
|
+
* SAP's default code-style. The trailing period (from a terminating `.` or
|
|
118
|
+
* `;` accidentally included by the regex match) is stripped defensively.
|
|
119
|
+
*/
|
|
120
|
+
function normalizeMethodName(name) {
|
|
121
|
+
return name.replace(/\.$/, '').trim().toLowerCase();
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Derive an alias from an entity name when the BDEF author omits `alias X`.
|
|
125
|
+
*
|
|
126
|
+
* RAP aliases are optional; if absent, SAP falls back to the entity name
|
|
127
|
+
* itself for handler-class derivation. We emulate that by stripping namespace
|
|
128
|
+
* prefixes (`/DMO/ZI_TRAVEL` → `ZI_TRAVEL`) and a short leading prefix like
|
|
129
|
+
* `ZI_` or `I_` (→ `TRAVEL`), then sanitizing any leftover non-identifier
|
|
130
|
+
* characters. Final fallback is `Entity` so the generated `lhc_entity`
|
|
131
|
+
* remains a valid ABAP identifier.
|
|
132
|
+
*/
|
|
133
|
+
function deriveAlias(entityName) {
|
|
134
|
+
const noNamespace = entityName.split('/').at(-1) ?? entityName;
|
|
135
|
+
const noPrefix = noNamespace.replace(/^[A-Z]{1,4}_/, '');
|
|
136
|
+
const normalized = (noPrefix || noNamespace).replace(/[^A-Za-z0-9_]/g, '');
|
|
137
|
+
return normalized || 'Entity';
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Split a BDEF into per-entity blocks bounded by `define behavior for ... { ... }`.
|
|
141
|
+
*
|
|
142
|
+
* A single interface BDEF can declare behavior for multiple entities
|
|
143
|
+
* (root + compositions), and each block has its own alias, its own actions,
|
|
144
|
+
* and produces its own `lhc_<alias>` handler class. We need the block
|
|
145
|
+
* boundaries so that an action declared under entity A isn't attributed to
|
|
146
|
+
* entity B's handler class.
|
|
147
|
+
*
|
|
148
|
+
* We track brace depth rather than simply splitting on `define behavior` so
|
|
149
|
+
* nested `{ ... }` inside features/draft/etag clauses doesn't close the block
|
|
150
|
+
* prematurely. `seenOpening` avoids closing a block before the first `{` is
|
|
151
|
+
* consumed — `define behavior for X` and the opening brace may be on
|
|
152
|
+
* separate lines.
|
|
153
|
+
*/
|
|
154
|
+
function parseBehaviorBlocks(source) {
|
|
155
|
+
const blocks = [];
|
|
156
|
+
const lines = source.split('\n');
|
|
157
|
+
let current;
|
|
158
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
159
|
+
const line = lines[i] ?? '';
|
|
160
|
+
if (!current) {
|
|
161
|
+
const defineMatch = line.match(BDEF_DEFINE_BEHAVIOR_RE);
|
|
162
|
+
if (!defineMatch)
|
|
163
|
+
continue;
|
|
164
|
+
const entityName = defineMatch[1] ?? '';
|
|
165
|
+
const alias = defineMatch[2] ?? deriveAlias(entityName);
|
|
166
|
+
current = {
|
|
167
|
+
entityName,
|
|
168
|
+
alias,
|
|
169
|
+
startLine: i + 1,
|
|
170
|
+
lines: [],
|
|
171
|
+
depth: 0,
|
|
172
|
+
seenOpening: false,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
current.lines.push(line);
|
|
176
|
+
if (line.includes('{'))
|
|
177
|
+
current.seenOpening = true;
|
|
178
|
+
current.depth += countChar(line, '{') - countChar(line, '}');
|
|
179
|
+
if (current.seenOpening && current.depth <= 0) {
|
|
180
|
+
blocks.push({
|
|
181
|
+
entityName: current.entityName,
|
|
182
|
+
alias: current.alias,
|
|
183
|
+
startLine: current.startLine,
|
|
184
|
+
lines: current.lines,
|
|
185
|
+
});
|
|
186
|
+
current = undefined;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return blocks;
|
|
190
|
+
}
|
|
191
|
+
function pushRequirement(out, requirement, seen) {
|
|
192
|
+
const key = rapHandlerRequirementKey(requirement);
|
|
193
|
+
if (seen.has(key))
|
|
194
|
+
return;
|
|
195
|
+
seen.add(key);
|
|
196
|
+
out.push(requirement);
|
|
197
|
+
}
|
|
198
|
+
function groupRequirementsByTargetClass(requirements) {
|
|
199
|
+
const grouped = new Map();
|
|
200
|
+
for (const req of requirements) {
|
|
201
|
+
const key = req.targetHandlerClass.toLowerCase();
|
|
202
|
+
const list = grouped.get(key) ?? [];
|
|
203
|
+
list.push(req);
|
|
204
|
+
grouped.set(key, list);
|
|
205
|
+
}
|
|
206
|
+
return grouped;
|
|
207
|
+
}
|
|
208
|
+
function hasActionResultClause(actionDeclaration) {
|
|
209
|
+
return /\bresult\b/i.test(actionDeclaration);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Extract RAP behavior-pool handler method requirements from interface BDEF source.
|
|
213
|
+
*
|
|
214
|
+
* For every behavior block (one per entity in the BDEF), this produces the
|
|
215
|
+
* exact METHOD signatures that the behavior pool's `lhc_<alias>` class must
|
|
216
|
+
* declare for the class to activate. The output is used by:
|
|
217
|
+
* - findMissingRapHandlerRequirements: to diff against an existing class
|
|
218
|
+
* - applyRapHandlerSignatures: to synthesize the missing METHODS lines
|
|
219
|
+
*
|
|
220
|
+
* The emitted signatures mirror what SAP's "Create Behavior Implementation"
|
|
221
|
+
* wizard would generate — same FOR MODIFY/FOR DETERMINE ON/FOR VALIDATE ON
|
|
222
|
+
* syntax, same `alias~method` entity reference, same RESULT clause only when
|
|
223
|
+
* the BDEF declares a `result` cardinality.
|
|
224
|
+
*/
|
|
225
|
+
export function extractRapHandlerRequirements(bdefSource) {
|
|
226
|
+
const requirements = [];
|
|
227
|
+
const seen = new Set();
|
|
228
|
+
const blocks = parseBehaviorBlocks(bdefSource);
|
|
229
|
+
for (const block of blocks) {
|
|
230
|
+
const alias = block.alias;
|
|
231
|
+
// RAP convention: one handler class per entity, named lhc_<alias>.
|
|
232
|
+
// This matches SAP's own templates and the ADT "Create Behavior
|
|
233
|
+
// Implementation Class" wizard — see /DMO/BP_TRAVEL_M and similar.
|
|
234
|
+
const targetHandlerClass = `lhc_${alias.toLowerCase()}`;
|
|
235
|
+
const body = block.lines.join('\n');
|
|
236
|
+
for (let idx = 0; idx < block.lines.length; idx += 1) {
|
|
237
|
+
const line = block.lines[idx] ?? '';
|
|
238
|
+
const declarationLine = block.startLine + idx;
|
|
239
|
+
// Match all BDEF action variants. Order of the optional prefixes matters:
|
|
240
|
+
// - `static` can appear alone or before `factory`: `static action`,
|
|
241
|
+
// `static factory action`
|
|
242
|
+
// - `internal` and `factory` are mutually exclusive but each may
|
|
243
|
+
// appear after `static`: `internal action`, `factory action`
|
|
244
|
+
// - the optional `( features: ... )` clause sits between the keyword
|
|
245
|
+
// `action` and the action name — `action ( features: instance ) Foo`
|
|
246
|
+
// Missing any of these prefixes used to silently drop the requirement,
|
|
247
|
+
// which was the original bug on live /DMO/BP_TRAVEL_M samples.
|
|
248
|
+
const actionMatch = line.match(BDEF_ACTION_DECLARATION_RE);
|
|
249
|
+
if (actionMatch?.[1]) {
|
|
250
|
+
const actionName = actionMatch[1];
|
|
251
|
+
const methodName = normalizeMethodName(actionName);
|
|
252
|
+
// Collapse continuation lines so the `result` clause is visible even
|
|
253
|
+
// when the author split the declaration across multiple lines.
|
|
254
|
+
const actionDecl = collectStatement(block.lines, idx);
|
|
255
|
+
// Emit `RESULT result` only when the BDEF declares a result cardinality.
|
|
256
|
+
// Factory/internal/static actions without a result clause must NOT
|
|
257
|
+
// carry RESULT in the handler signature — the activation check is strict
|
|
258
|
+
// and rejects mismatched signatures with a cryptic "method signature
|
|
259
|
+
// does not match BDL declaration" error.
|
|
260
|
+
const hasResult = hasActionResultClause(actionDecl);
|
|
261
|
+
const resultPart = hasResult ? ' RESULT result' : '';
|
|
262
|
+
pushRequirement(requirements, {
|
|
263
|
+
kind: 'action',
|
|
264
|
+
methodName,
|
|
265
|
+
entityName: block.entityName,
|
|
266
|
+
entityAlias: alias,
|
|
267
|
+
targetHandlerClass,
|
|
268
|
+
declarationLine,
|
|
269
|
+
signature: `METHODS ${methodName} FOR MODIFY\n` + ` IMPORTING keys FOR ACTION ${alias}~${actionName}${resultPart}.`,
|
|
270
|
+
}, seen);
|
|
271
|
+
}
|
|
272
|
+
const determinationMatch = line.match(BDEF_DETERMINATION_DECLARATION_RE);
|
|
273
|
+
if (determinationMatch?.[1] && determinationMatch[2]) {
|
|
274
|
+
const determinationName = determinationMatch[1];
|
|
275
|
+
const event = determinationMatch[2].toUpperCase();
|
|
276
|
+
const methodName = normalizeMethodName(determinationName);
|
|
277
|
+
pushRequirement(requirements, {
|
|
278
|
+
kind: 'determination',
|
|
279
|
+
methodName,
|
|
280
|
+
entityName: block.entityName,
|
|
281
|
+
entityAlias: alias,
|
|
282
|
+
targetHandlerClass,
|
|
283
|
+
declarationLine,
|
|
284
|
+
signature: `METHODS ${methodName} FOR DETERMINE ON ${event}\n` +
|
|
285
|
+
` IMPORTING keys FOR ${alias}~${determinationName}.`,
|
|
286
|
+
}, seen);
|
|
287
|
+
}
|
|
288
|
+
const validationMatch = line.match(BDEF_VALIDATION_DECLARATION_RE);
|
|
289
|
+
if (validationMatch?.[1] && validationMatch[2]) {
|
|
290
|
+
const validationName = validationMatch[1];
|
|
291
|
+
const event = validationMatch[2].toUpperCase();
|
|
292
|
+
const methodName = normalizeMethodName(validationName);
|
|
293
|
+
pushRequirement(requirements, {
|
|
294
|
+
kind: 'validation',
|
|
295
|
+
methodName,
|
|
296
|
+
entityName: block.entityName,
|
|
297
|
+
entityAlias: alias,
|
|
298
|
+
targetHandlerClass,
|
|
299
|
+
declarationLine,
|
|
300
|
+
signature: `METHODS ${methodName} FOR VALIDATE ON ${event}\n` + ` IMPORTING keys FOR ${alias}~${validationName}.`,
|
|
301
|
+
}, seen);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// `authorization master ( instance )` → the pool must implement
|
|
305
|
+
// get_instance_authorizations (per-instance row-level checks, imports
|
|
306
|
+
// keys so the handler can evaluate each row individually).
|
|
307
|
+
const instanceAuthMatch = body.match(BDEF_INSTANCE_AUTH_RE);
|
|
308
|
+
if (instanceAuthMatch) {
|
|
309
|
+
pushRequirement(requirements, {
|
|
310
|
+
kind: 'instance_authorization',
|
|
311
|
+
methodName: 'get_instance_authorizations',
|
|
312
|
+
entityName: block.entityName,
|
|
313
|
+
entityAlias: alias,
|
|
314
|
+
targetHandlerClass,
|
|
315
|
+
declarationLine: block.startLine,
|
|
316
|
+
signature: 'METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION\n' +
|
|
317
|
+
` IMPORTING keys REQUEST requested_authorizations FOR ${alias} RESULT result.`,
|
|
318
|
+
}, seen);
|
|
319
|
+
}
|
|
320
|
+
// `authorization master ( global )` → the pool must implement
|
|
321
|
+
// get_global_authorizations (a single, stateless check for the whole
|
|
322
|
+
// entity; no keys parameter because the decision is not per-row).
|
|
323
|
+
const globalAuthMatch = body.match(BDEF_GLOBAL_AUTH_RE);
|
|
324
|
+
if (globalAuthMatch) {
|
|
325
|
+
pushRequirement(requirements, {
|
|
326
|
+
kind: 'global_authorization',
|
|
327
|
+
methodName: 'get_global_authorizations',
|
|
328
|
+
entityName: block.entityName,
|
|
329
|
+
entityAlias: alias,
|
|
330
|
+
targetHandlerClass,
|
|
331
|
+
declarationLine: block.startLine,
|
|
332
|
+
signature: 'METHODS get_global_authorizations FOR GLOBAL AUTHORIZATION\n' +
|
|
333
|
+
` IMPORTING REQUEST requested_authorizations FOR ${alias} RESULT result.`,
|
|
334
|
+
}, seen);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return requirements;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Find every `CLASS ... DEFINITION` block in an ABAP source, returning the
|
|
341
|
+
* line index range and — if present — the line index of PRIVATE SECTION.
|
|
342
|
+
*
|
|
343
|
+
* Behavior pool sources frequently contain:
|
|
344
|
+
* - multiple concrete handler classes (`lhc_travel`, `lhc_booking`, ...)
|
|
345
|
+
* - deferred declarations (`CLASS lhc_travel DEFINITION DEFERRED.`) used
|
|
346
|
+
* to satisfy forward references in the implementation section; these
|
|
347
|
+
* have no matching ENDCLASS and must not be confused with the real
|
|
348
|
+
* definition that follows later in the same file
|
|
349
|
+
*
|
|
350
|
+
* The `i = end` advance at the bottom skips past the ENDCLASS of the class
|
|
351
|
+
* we just processed, so the outer loop doesn't re-enter the same class and
|
|
352
|
+
* double-register ranges.
|
|
353
|
+
*/
|
|
354
|
+
function parseClassDefinitionRanges(source) {
|
|
355
|
+
const lines = source.split('\n');
|
|
356
|
+
const ranges = [];
|
|
357
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
358
|
+
const line = lines[i] ?? '';
|
|
359
|
+
const startMatch = line.match(CLASS_DEFINITION_START_RE);
|
|
360
|
+
if (!startMatch?.[1])
|
|
361
|
+
continue;
|
|
362
|
+
const name = startMatch[1];
|
|
363
|
+
const isDeferred = CLASS_DEFINITION_DEFERRED_RE.test(line);
|
|
364
|
+
if (isDeferred)
|
|
365
|
+
continue;
|
|
366
|
+
let end = i;
|
|
367
|
+
let privateSection;
|
|
368
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
369
|
+
const inner = lines[j] ?? '';
|
|
370
|
+
if (privateSection === undefined && PRIVATE_SECTION_RE.test(inner)) {
|
|
371
|
+
privateSection = j;
|
|
372
|
+
}
|
|
373
|
+
if (ENDCLASS_RE.test(inner)) {
|
|
374
|
+
end = j;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
ranges.push({ name, start: i, end, privateSection });
|
|
379
|
+
i = end;
|
|
380
|
+
}
|
|
381
|
+
return ranges;
|
|
382
|
+
}
|
|
383
|
+
function parseClassImplementationRanges(source) {
|
|
384
|
+
const lines = source.split('\n');
|
|
385
|
+
const ranges = [];
|
|
386
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
387
|
+
const line = lines[i] ?? '';
|
|
388
|
+
const startMatch = line.match(CLASS_IMPLEMENTATION_START_RE);
|
|
389
|
+
if (!startMatch?.[1])
|
|
390
|
+
continue;
|
|
391
|
+
const name = startMatch[1];
|
|
392
|
+
let end = i;
|
|
393
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
394
|
+
const inner = lines[j] ?? '';
|
|
395
|
+
if (ENDCLASS_RE.test(inner)) {
|
|
396
|
+
end = j;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
ranges.push({ name, start: i, end });
|
|
401
|
+
i = end;
|
|
402
|
+
}
|
|
403
|
+
return ranges;
|
|
404
|
+
}
|
|
405
|
+
function parseClassImplementationMethods(source) {
|
|
406
|
+
const lines = source.split('\n');
|
|
407
|
+
const ranges = parseClassImplementationRanges(source);
|
|
408
|
+
const out = new Map();
|
|
409
|
+
for (const range of ranges) {
|
|
410
|
+
const key = range.name.toLowerCase();
|
|
411
|
+
const methods = out.get(key) ?? new Set();
|
|
412
|
+
for (let i = range.start; i <= range.end; i += 1) {
|
|
413
|
+
const line = lines[i] ?? '';
|
|
414
|
+
const match = line.match(METHOD_IMPLEMENTATION_RE);
|
|
415
|
+
if (match?.[1])
|
|
416
|
+
methods.add(normalizeMethodName(match[1]));
|
|
417
|
+
}
|
|
418
|
+
out.set(key, methods);
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
function parseHandlerDeclarationBinding(statement) {
|
|
423
|
+
const actionBinding = statement.match(HANDLER_ACTION_BINDING_RE);
|
|
424
|
+
if (actionBinding?.[1] && actionBinding[2]) {
|
|
425
|
+
return { kind: 'action', entityAlias: actionBinding[1], methodName: actionBinding[2] };
|
|
426
|
+
}
|
|
427
|
+
const entityBinding = statement.match(HANDLER_ENTITY_BINDING_RE);
|
|
428
|
+
if (entityBinding?.[1] && entityBinding[2]) {
|
|
429
|
+
if (HANDLER_DETERMINE_STATEMENT_RE.test(statement)) {
|
|
430
|
+
return { kind: 'determination', entityAlias: entityBinding[1], methodName: entityBinding[2] };
|
|
431
|
+
}
|
|
432
|
+
if (HANDLER_VALIDATE_STATEMENT_RE.test(statement)) {
|
|
433
|
+
return { kind: 'validation', entityAlias: entityBinding[1], methodName: entityBinding[2] };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const authBinding = statement.match(HANDLER_AUTH_BINDING_RE);
|
|
437
|
+
if (authBinding?.[1]) {
|
|
438
|
+
if (HANDLER_INSTANCE_AUTH_STATEMENT_RE.test(statement)) {
|
|
439
|
+
return {
|
|
440
|
+
kind: 'instance_authorization',
|
|
441
|
+
entityAlias: authBinding[1],
|
|
442
|
+
methodName: 'get_instance_authorizations',
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
if (HANDLER_GLOBAL_AUTH_STATEMENT_RE.test(statement)) {
|
|
446
|
+
return {
|
|
447
|
+
kind: 'global_authorization',
|
|
448
|
+
entityAlias: authBinding[1],
|
|
449
|
+
methodName: 'get_global_authorizations',
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Map BDEF requirement keys to the concrete ABAP method name used in the
|
|
457
|
+
* handler declaration.
|
|
458
|
+
*
|
|
459
|
+
* The generated method name for a BDEF action `acceptTravel` is `accepttravel`,
|
|
460
|
+
* but real behavior pools often declare semantic method names such as
|
|
461
|
+
* `set_status_accepted FOR ACTION travel~acceptTravel`. Stub detection and
|
|
462
|
+
* stub generation must use the declared ABAP method name, otherwise a pool
|
|
463
|
+
* that is already implemented under semantic names is reported as missing.
|
|
464
|
+
*/
|
|
465
|
+
function parseClassDefinitionHandlerBindings(source) {
|
|
466
|
+
const lines = source.split('\n');
|
|
467
|
+
const ranges = parseClassDefinitionRanges(source);
|
|
468
|
+
const out = new Map();
|
|
469
|
+
for (const range of ranges) {
|
|
470
|
+
const targetHandlerClass = range.name;
|
|
471
|
+
for (let i = range.start; i <= range.end; i += 1) {
|
|
472
|
+
const line = lines[i] ?? '';
|
|
473
|
+
const match = line.match(METHOD_DECLARATION_RE);
|
|
474
|
+
if (!match?.[1])
|
|
475
|
+
continue;
|
|
476
|
+
const declaredMethodName = normalizeMethodName(match[1]);
|
|
477
|
+
const statement = collectAbapStatement(lines, i, range.end);
|
|
478
|
+
const binding = parseHandlerDeclarationBinding(statement);
|
|
479
|
+
if (binding)
|
|
480
|
+
out.set(bindingKey(targetHandlerClass, binding.kind, binding.methodName, binding.entityAlias), declaredMethodName);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Parse method declarations (`METHODS ...`) per class definition.
|
|
487
|
+
*
|
|
488
|
+
* The returned Set contains BOTH:
|
|
489
|
+
* 1. Every declared METHOD name in the class (e.g. `submitforapproval`,
|
|
490
|
+
* `set_status_accepted`, `validate_customer`)
|
|
491
|
+
* 2. Every RAP binding-key those methods are bound to (the action /
|
|
492
|
+
* determination / validation / authorization referenced in the
|
|
493
|
+
* `FOR ACTION <alias>~<name>` / `FOR <alias>~<name>` /
|
|
494
|
+
* `FOR INSTANCE AUTHORIZATION ... FOR <alias>` clauses)
|
|
495
|
+
*
|
|
496
|
+
* Why both? Hand-crafted behavior pools (like SAP's own /DMO/BP_TRAVEL_M)
|
|
497
|
+
* routinely use semantic method names that differ from the BDEF action
|
|
498
|
+
* names — e.g. BDEF `action acceptTravel` bound to METHOD
|
|
499
|
+
* `set_status_accepted` via `FOR ACTION travel~acceptTravel`. The
|
|
500
|
+
* scaffolder's missing-requirement check compares by BDEF identifier, so if
|
|
501
|
+
* we only indexed method names, it would incorrectly report
|
|
502
|
+
* `accepttravel` as missing and try to inject a duplicate METHOD line.
|
|
503
|
+
*
|
|
504
|
+
* METHOD declarations can span multiple lines (one line for the name +
|
|
505
|
+
* continuation lines for FOR / IMPORTING / RESULT), so we join the
|
|
506
|
+
* statement up to its terminating `.` before pattern-matching the binding.
|
|
507
|
+
*/
|
|
508
|
+
export function parseClassDefinitionMethods(source) {
|
|
509
|
+
const lines = source.split('\n');
|
|
510
|
+
const ranges = parseClassDefinitionRanges(source);
|
|
511
|
+
const out = new Map();
|
|
512
|
+
for (const range of ranges) {
|
|
513
|
+
const key = range.name.toLowerCase();
|
|
514
|
+
const methods = out.get(key) ?? new Set();
|
|
515
|
+
for (let i = range.start; i <= range.end; i += 1) {
|
|
516
|
+
const line = lines[i] ?? '';
|
|
517
|
+
const match = line.match(METHOD_DECLARATION_RE);
|
|
518
|
+
if (!match?.[1])
|
|
519
|
+
continue;
|
|
520
|
+
methods.add(normalizeMethodName(match[1]));
|
|
521
|
+
// Collect the full multi-line METHODS statement so FOR-clause patterns
|
|
522
|
+
// (which usually sit on a continuation line) are visible to the regex.
|
|
523
|
+
const statement = collectAbapStatement(lines, i, range.end);
|
|
524
|
+
// Also index the BDEF-side binding key. This is different from the
|
|
525
|
+
// ABAP method name when developers use semantic names such as
|
|
526
|
+
// `set_status_accepted FOR ACTION travel~acceptTravel`.
|
|
527
|
+
const binding = parseHandlerDeclarationBinding(statement);
|
|
528
|
+
if (binding)
|
|
529
|
+
methods.add(normalizeMethodName(binding.methodName));
|
|
530
|
+
}
|
|
531
|
+
out.set(key, methods);
|
|
532
|
+
}
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Determine which RAP handler requirements are missing from class definitions.
|
|
537
|
+
*
|
|
538
|
+
* If the target handler class (`lhc_<alias>`) doesn't exist in the source at
|
|
539
|
+
* all, every requirement for that class is reported missing so the caller
|
|
540
|
+
* can decide whether to create the class or fall through to another include
|
|
541
|
+
* (the scaffold flow searches `main` → `definitions` → `implementations`).
|
|
542
|
+
*
|
|
543
|
+
* Method-name comparison is case-insensitive because ABAP identifiers
|
|
544
|
+
* are — we normalize on both sides so `METHODS SubmitForApproval ...`
|
|
545
|
+
* matches a BDEF `action SubmitForApproval`.
|
|
546
|
+
*/
|
|
547
|
+
export function findMissingRapHandlerRequirements(requirements, classSource) {
|
|
548
|
+
const classMethods = parseClassDefinitionMethods(classSource);
|
|
549
|
+
return requirements.filter((req) => {
|
|
550
|
+
const methods = classMethods.get(req.targetHandlerClass.toLowerCase());
|
|
551
|
+
if (!methods)
|
|
552
|
+
return true;
|
|
553
|
+
return !methods.has(normalizeMethodName(req.methodName));
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
export function findMissingRapHandlerImplementationStubs(requirements, classSource) {
|
|
557
|
+
const classMethods = parseClassImplementationMethods(classSource);
|
|
558
|
+
const declaredMethodByRequirement = parseClassDefinitionHandlerBindings(classSource);
|
|
559
|
+
return requirements.filter((req) => {
|
|
560
|
+
const methods = classMethods.get(req.targetHandlerClass.toLowerCase());
|
|
561
|
+
if (!methods)
|
|
562
|
+
return true;
|
|
563
|
+
const implementationMethodName = declaredMethodByRequirement.get(rapHandlerRequirementKey(req)) ?? normalizeMethodName(req.methodName);
|
|
564
|
+
return !methods.has(implementationMethodName);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Insert missing RAP handler signatures into matching `lhc_*` class definitions.
|
|
569
|
+
*
|
|
570
|
+
* Scope and contract:
|
|
571
|
+
* - Only DEFINITION sections are modified. Use
|
|
572
|
+
* applyRapHandlerImplementationStubs after this step when the scaffold
|
|
573
|
+
* should be immediately patchable with edit_method.
|
|
574
|
+
* - Requirements whose target class (`lhc_<alias>`) is not present in this
|
|
575
|
+
* source are returned in `skipped`, not silently dropped. The caller can
|
|
576
|
+
* then try another include (definitions/implementations) or surface a
|
|
577
|
+
* clear error to the user.
|
|
578
|
+
* - Edits are applied bottom-up (highest line index first) so that earlier
|
|
579
|
+
* splice operations don't shift the indices of later ones.
|
|
580
|
+
* - When a target class exists but has no PRIVATE SECTION at all, the
|
|
581
|
+
* entire PRIVATE SECTION with the signatures is inserted just before
|
|
582
|
+
* ENDCLASS — this covers freshly-generated behavior pools where ADT
|
|
583
|
+
* produced a skeleton without method declarations.
|
|
584
|
+
*/
|
|
585
|
+
export function applyRapHandlerSignatures(classSource, requirements) {
|
|
586
|
+
if (requirements.length === 0) {
|
|
587
|
+
return { updatedSource: classSource, inserted: [], skipped: [], changed: false };
|
|
588
|
+
}
|
|
589
|
+
const lines = classSource.split('\n');
|
|
590
|
+
const ranges = parseClassDefinitionRanges(classSource);
|
|
591
|
+
const methodsByClass = parseClassDefinitionMethods(classSource);
|
|
592
|
+
const grouped = groupRequirementsByTargetClass(requirements);
|
|
593
|
+
const edits = [];
|
|
594
|
+
const inserted = [];
|
|
595
|
+
const skipped = [];
|
|
596
|
+
for (const [targetClassName, classRequirements] of grouped.entries()) {
|
|
597
|
+
const range = ranges.find((r) => r.name.toLowerCase() === targetClassName);
|
|
598
|
+
if (!range) {
|
|
599
|
+
for (const req of classRequirements) {
|
|
600
|
+
skipped.push({
|
|
601
|
+
requirement: req,
|
|
602
|
+
reason: `Handler class ${req.targetHandlerClass} not found in behavior pool.`,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
const existingMethods = methodsByClass.get(targetClassName) ?? new Set();
|
|
608
|
+
const toInsert = classRequirements.filter((req) => !existingMethods.has(normalizeMethodName(req.methodName)));
|
|
609
|
+
if (toInsert.length === 0)
|
|
610
|
+
continue;
|
|
611
|
+
const signatureLines = [];
|
|
612
|
+
for (let i = 0; i < toInsert.length; i += 1) {
|
|
613
|
+
const req = toInsert[i];
|
|
614
|
+
signatureLines.push(...req.signature.split('\n').map((line) => ` ${line}`));
|
|
615
|
+
if (i < toInsert.length - 1)
|
|
616
|
+
signatureLines.push('');
|
|
617
|
+
inserted.push(req);
|
|
618
|
+
}
|
|
619
|
+
if (range.privateSection === undefined) {
|
|
620
|
+
const block = [' PRIVATE SECTION.', ...signatureLines, ''];
|
|
621
|
+
edits.push({ index: range.end, lines: block });
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
edits.push({
|
|
625
|
+
index: range.privateSection + 1,
|
|
626
|
+
lines: [...signatureLines, ''],
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
if (edits.length === 0) {
|
|
630
|
+
return { updatedSource: classSource, inserted, skipped, changed: false };
|
|
631
|
+
}
|
|
632
|
+
const sorted = edits.sort((a, b) => b.index - a.index);
|
|
633
|
+
for (const edit of sorted) {
|
|
634
|
+
lines.splice(edit.index, 0, ...edit.lines);
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
updatedSource: lines.join('\n'),
|
|
638
|
+
inserted,
|
|
639
|
+
skipped,
|
|
640
|
+
changed: inserted.length > 0,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
export function applyRapHandlerImplementationStubs(classSource, requirements, options = {}) {
|
|
644
|
+
if (requirements.length === 0) {
|
|
645
|
+
return { updatedSource: classSource, inserted: [], skipped: [], changed: false };
|
|
646
|
+
}
|
|
647
|
+
const lines = classSource.split('\n');
|
|
648
|
+
const definitionRanges = parseClassDefinitionRanges(classSource);
|
|
649
|
+
const implementationRanges = parseClassImplementationRanges(classSource);
|
|
650
|
+
const methodsByClass = parseClassImplementationMethods(classSource);
|
|
651
|
+
const definitionLookupSource = options.definitionSource ? `${options.definitionSource}\n${classSource}` : classSource;
|
|
652
|
+
const declaredMethodByRequirement = parseClassDefinitionHandlerBindings(definitionLookupSource);
|
|
653
|
+
const grouped = groupRequirementsByTargetClass(requirements);
|
|
654
|
+
const edits = [];
|
|
655
|
+
const inserted = [];
|
|
656
|
+
const skipped = [];
|
|
657
|
+
for (const [targetClassName, classRequirements] of grouped.entries()) {
|
|
658
|
+
const existingMethods = methodsByClass.get(targetClassName) ?? new Set();
|
|
659
|
+
const seenMethods = new Set(existingMethods);
|
|
660
|
+
const toInsert = [];
|
|
661
|
+
for (const req of classRequirements) {
|
|
662
|
+
const implementationMethodName = declaredMethodByRequirement.get(rapHandlerRequirementKey(req)) ?? normalizeMethodName(req.methodName);
|
|
663
|
+
if (seenMethods.has(implementationMethodName))
|
|
664
|
+
continue;
|
|
665
|
+
seenMethods.add(implementationMethodName);
|
|
666
|
+
toInsert.push({ requirement: req, implementationMethodName });
|
|
667
|
+
}
|
|
668
|
+
if (toInsert.length === 0)
|
|
669
|
+
continue;
|
|
670
|
+
const stubLines = [];
|
|
671
|
+
for (let i = 0; i < toInsert.length; i += 1) {
|
|
672
|
+
const { requirement, implementationMethodName } = toInsert[i];
|
|
673
|
+
stubLines.push(` METHOD ${implementationMethodName}.`, ' ENDMETHOD.');
|
|
674
|
+
if (i < toInsert.length - 1)
|
|
675
|
+
stubLines.push('');
|
|
676
|
+
inserted.push(requirement);
|
|
677
|
+
}
|
|
678
|
+
const implementationRange = implementationRanges.find((r) => r.name.toLowerCase() === targetClassName);
|
|
679
|
+
if (implementationRange) {
|
|
680
|
+
edits.push({ index: implementationRange.end, lines: [...stubLines, ''] });
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
const hasDefinition = definitionRanges.some((r) => r.name.toLowerCase() === targetClassName);
|
|
684
|
+
if (options.createImplementationBlocks && hasDefinition) {
|
|
685
|
+
edits.push({
|
|
686
|
+
index: lines.length,
|
|
687
|
+
lines: ['', `CLASS ${classRequirements[0].targetHandlerClass} IMPLEMENTATION.`, ...stubLines, 'ENDCLASS.'],
|
|
688
|
+
});
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
for (const { requirement } of toInsert) {
|
|
692
|
+
skipped.push({
|
|
693
|
+
requirement,
|
|
694
|
+
reason: `Implementation class ${requirement.targetHandlerClass} not found in behavior pool.`,
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
inserted.splice(inserted.length - toInsert.length, toInsert.length);
|
|
698
|
+
}
|
|
699
|
+
if (edits.length === 0) {
|
|
700
|
+
return { updatedSource: classSource, inserted, skipped, changed: false };
|
|
701
|
+
}
|
|
702
|
+
const sorted = edits.sort((a, b) => b.index - a.index);
|
|
703
|
+
for (const edit of sorted) {
|
|
704
|
+
lines.splice(edit.index, 0, ...edit.lines);
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
updatedSource: lines.join('\n'),
|
|
708
|
+
inserted,
|
|
709
|
+
skipped,
|
|
710
|
+
changed: inserted.length > 0,
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function countInserted(results) {
|
|
714
|
+
return (results.main.inserted.length +
|
|
715
|
+
(results.definitions?.inserted.length ?? 0) +
|
|
716
|
+
(results.implementations?.inserted.length ?? 0));
|
|
717
|
+
}
|
|
718
|
+
function changedSectionsFrom(changed) {
|
|
719
|
+
return Object.keys(changed).filter((section) => changed[section]);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Build the source used to resolve semantic method names while creating stubs.
|
|
723
|
+
*
|
|
724
|
+
* The declaration (`METHODS set_status_accepted FOR ACTION travel~acceptTravel`)
|
|
725
|
+
* and implementation (`METHOD set_status_accepted.`) can live in different ADT
|
|
726
|
+
* includes. Stub generation therefore needs the post-signature sources for all
|
|
727
|
+
* sections, not only the include currently being edited.
|
|
728
|
+
*/
|
|
729
|
+
function buildDefinitionLookupSource(originalSections, signaturePlan) {
|
|
730
|
+
return [
|
|
731
|
+
signaturePlan.updatedSections.main,
|
|
732
|
+
signaturePlan.updatedSections.definitions ?? originalSections.definitions,
|
|
733
|
+
signaturePlan.updatedSections.implementations ?? originalSections.implementations,
|
|
734
|
+
]
|
|
735
|
+
.filter(Boolean)
|
|
736
|
+
.join('\n\n');
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Try signature insertion in ADT include order: main → definitions → implementations.
|
|
740
|
+
*
|
|
741
|
+
* A handler class can legally exist in any of these includes depending on how
|
|
742
|
+
* ADT generated the behavior pool. The unresolved list is deliberately carried
|
|
743
|
+
* forward between sections so a requirement is inserted exactly once, in the
|
|
744
|
+
* first include that contains its concrete `lhc_<alias> DEFINITION`.
|
|
745
|
+
*/
|
|
746
|
+
function applySignaturesAcrossSections(sections, missingSignatures) {
|
|
747
|
+
const main = applyRapHandlerSignatures(sections.main, missingSignatures);
|
|
748
|
+
let unresolved = main.skipped.map((entry) => entry.requirement);
|
|
749
|
+
let definitions;
|
|
750
|
+
if (unresolved.length > 0 && sections.definitions) {
|
|
751
|
+
definitions = applyRapHandlerSignatures(sections.definitions, unresolved);
|
|
752
|
+
unresolved = definitions.skipped.map((entry) => entry.requirement);
|
|
753
|
+
}
|
|
754
|
+
let implementations;
|
|
755
|
+
if (unresolved.length > 0 && sections.implementations) {
|
|
756
|
+
implementations = applyRapHandlerSignatures(sections.implementations, unresolved);
|
|
757
|
+
unresolved = implementations.skipped.map((entry) => entry.requirement);
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
signatures: { main, definitions, implementations },
|
|
761
|
+
updatedSections: {
|
|
762
|
+
main: main.updatedSource,
|
|
763
|
+
definitions: definitions?.updatedSource ?? sections.definitions,
|
|
764
|
+
implementations: implementations?.updatedSource ?? sections.implementations,
|
|
765
|
+
},
|
|
766
|
+
unresolved,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Build the complete auto-apply plan for a behavior pool without doing I/O.
|
|
771
|
+
*
|
|
772
|
+
* This is intentionally pure: the MCP handler owns safety checks, locks,
|
|
773
|
+
* linting, and ADT writes; this helper owns the RAP-specific sequencing:
|
|
774
|
+
* 1. insert missing METHODS declarations into whichever include contains
|
|
775
|
+
* the matching `lhc_<alias> DEFINITION`
|
|
776
|
+
* 2. skip stubs whose declarations still could not be placed
|
|
777
|
+
* 3. insert empty METHOD stubs using the concrete ABAP method name from
|
|
778
|
+
* existing/new declarations, including semantic names bound via
|
|
779
|
+
* `FOR ACTION alias~ActionName`
|
|
780
|
+
*
|
|
781
|
+
* Keeping this plan here prevents `intent.ts` from duplicating RAP parser
|
|
782
|
+
* invariants such as "deferred classes are not editable" and "stub method
|
|
783
|
+
* names come from declarations, not necessarily from BDEF action names".
|
|
784
|
+
*/
|
|
785
|
+
export function applyRapHandlerScaffold(sections, missingSignatures, missingImplementationStubs) {
|
|
786
|
+
const signaturePlan = applySignaturesAcrossSections(sections, missingSignatures);
|
|
787
|
+
// A METHOD stub is only useful after its declaration exists. If the target
|
|
788
|
+
// `lhc_*` class was not found anywhere, suppress the stub for that unresolved
|
|
789
|
+
// declaration rather than creating an implementation block with no matching
|
|
790
|
+
// RAP handler signature.
|
|
791
|
+
const unresolvedDeclarationKeys = new Set(signaturePlan.unresolved.map(rapHandlerRequirementKey));
|
|
792
|
+
const stubRequirements = missingImplementationStubs.filter((req) => !unresolvedDeclarationKeys.has(rapHandlerRequirementKey(req)));
|
|
793
|
+
const definitionLookupSource = buildDefinitionLookupSource(sections, signaturePlan);
|
|
794
|
+
const stubMain = applyRapHandlerImplementationStubs(signaturePlan.updatedSections.main, stubRequirements, {
|
|
795
|
+
createImplementationBlocks: true,
|
|
796
|
+
definitionSource: definitionLookupSource,
|
|
797
|
+
});
|
|
798
|
+
const stubDefinitions = sections.definitions
|
|
799
|
+
? applyRapHandlerImplementationStubs(signaturePlan.updatedSections.definitions ?? sections.definitions, stubRequirements, {
|
|
800
|
+
definitionSource: definitionLookupSource,
|
|
801
|
+
})
|
|
802
|
+
: undefined;
|
|
803
|
+
const stubImplementations = sections.implementations
|
|
804
|
+
? applyRapHandlerImplementationStubs(signaturePlan.updatedSections.implementations ?? sections.implementations, stubRequirements, { createImplementationBlocks: true, definitionSource: definitionLookupSource })
|
|
805
|
+
: undefined;
|
|
806
|
+
const changed = {
|
|
807
|
+
main: signaturePlan.signatures.main.changed || stubMain.changed,
|
|
808
|
+
definitions: (signaturePlan.signatures.definitions?.changed ?? false) || (stubDefinitions?.changed ?? false),
|
|
809
|
+
implementations: (signaturePlan.signatures.implementations?.changed ?? false) || (stubImplementations?.changed ?? false),
|
|
810
|
+
};
|
|
811
|
+
const changedSections = changedSectionsFrom(changed);
|
|
812
|
+
return {
|
|
813
|
+
sections: {
|
|
814
|
+
main: stubMain.updatedSource,
|
|
815
|
+
definitions: stubDefinitions?.updatedSource ?? signaturePlan.updatedSections.definitions,
|
|
816
|
+
implementations: stubImplementations?.updatedSource ?? signaturePlan.updatedSections.implementations,
|
|
817
|
+
},
|
|
818
|
+
signatures: signaturePlan.signatures,
|
|
819
|
+
implementationStubs: {
|
|
820
|
+
main: stubMain,
|
|
821
|
+
definitions: stubDefinitions,
|
|
822
|
+
implementations: stubImplementations,
|
|
823
|
+
},
|
|
824
|
+
unresolved: signaturePlan.unresolved,
|
|
825
|
+
changed,
|
|
826
|
+
changedSections,
|
|
827
|
+
insertedSignatureCount: countInserted(signaturePlan.signatures),
|
|
828
|
+
insertedImplementationStubCount: countInserted({
|
|
829
|
+
main: stubMain,
|
|
830
|
+
definitions: stubDefinitions,
|
|
831
|
+
implementations: stubImplementations,
|
|
832
|
+
}),
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
//# sourceMappingURL=rap-handlers.js.map
|