abapgit-agent 1.14.5 → 1.15.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/abap/CLAUDE.md +34 -4
- package/abap/guidelines/abapgit-fugr.md +159 -0
- package/abap/guidelines/abapgit-xml-only.md +397 -0
- package/abap/guidelines/abapgit.md +29 -304
- package/abap/guidelines/abaplint-local.md +1 -1
- package/abap/guidelines/abaplint.md +1 -1
- package/abap/guidelines/branch-workflow.md +1 -1
- package/abap/guidelines/cds-testing.md +1 -1
- package/abap/guidelines/common-errors.md +1 -1
- package/abap/guidelines/debug-dump.md +1 -1
- package/abap/guidelines/debug-session.md +23 -24
- package/abap/guidelines/object-creation.md +1 -1
- package/abap/guidelines/probe-poc.md +1 -1
- package/abap/guidelines/run-probe-classes.md +1 -1
- package/abap/guidelines/unit-testable-code.md +1 -43
- package/abap/guidelines/workflow-detailed.md +1 -1
- package/package.json +2 -1
- package/src/commands/debug.js +48 -6
- package/src/commands/view.js +63 -2
- package/src/utils/abap-reference.js +2 -0
package/src/commands/debug.js
CHANGED
|
@@ -89,6 +89,10 @@ const INCLUDE_TYPE_TO_ADT = {
|
|
|
89
89
|
* e.g. ZCL_ABGAGT_AGENT=============CM00D
|
|
90
90
|
* These must be routed to the programs/includes ADT endpoint.
|
|
91
91
|
*
|
|
92
|
+
* FUGR source includes follow L<group><suffix> naming (e.g. LSUSRU04, LSUSRTOP,
|
|
93
|
+
* LSUSRF10, L_ABAU01). They start with 'L' and are routed to programs/includes.
|
|
94
|
+
* Customer-namespace programs always start with Z/Y, so this branch is safe.
|
|
95
|
+
*
|
|
92
96
|
* When includeType is supplied (testclasses|locals_imp|locals_def),
|
|
93
97
|
* the URI targets the sub-include of the class instead of /source/main.
|
|
94
98
|
* Line numbers are then section-local (from the .clas.<file>.abap file).
|
|
@@ -103,6 +107,26 @@ function objectUri(name, includeType) {
|
|
|
103
107
|
}
|
|
104
108
|
return `/sap/bc/adt/oo/classes/${lower}/source/main`;
|
|
105
109
|
}
|
|
110
|
+
// FUGR source includes: L<group>U<NN>, L<group>TOP, L<group>F<NN>, etc.
|
|
111
|
+
// All start with 'L'. Customer Z/Y programs never start with 'L'.
|
|
112
|
+
// Correct ADT URI (verified against abap-adt-api):
|
|
113
|
+
// /sap/bc/adt/functions/groups/<group>/includes/<include>/source/main
|
|
114
|
+
// NOT /programs/includes/ — that path is for standalone PROG/I includes only.
|
|
115
|
+
// Group name is derived by stripping leading 'L' and trailing suffix:
|
|
116
|
+
// U<NN> — FM source include (U01, U02, ...)
|
|
117
|
+
// TOP — pool include
|
|
118
|
+
// F<NN> — form include
|
|
119
|
+
// XX — internal include
|
|
120
|
+
if (/^L/.test(upper)) {
|
|
121
|
+
const withoutL = upper.slice(1); // e.g. ZCAIS_DEMOU01
|
|
122
|
+
const group = withoutL
|
|
123
|
+
.replace(/U\d+$/, '') // strip Unn suffix
|
|
124
|
+
.replace(/TOP$/, '') // strip TOP suffix
|
|
125
|
+
.replace(/F\d+$/, '') // strip Fnn suffix
|
|
126
|
+
.replace(/XX$/, '') // strip XX suffix
|
|
127
|
+
.toLowerCase();
|
|
128
|
+
return `/sap/bc/adt/functions/groups/${group}/includes/${lower}/source/main`;
|
|
129
|
+
}
|
|
106
130
|
return `/sap/bc/adt/programs/programs/${lower}`;
|
|
107
131
|
}
|
|
108
132
|
|
|
@@ -227,15 +251,25 @@ async function refreshBreakpoints(config, adt, bps) {
|
|
|
227
251
|
|
|
228
252
|
const serverResults = parseBreakpointResponse(resp.body || '', bps);
|
|
229
253
|
|
|
230
|
-
// Match server results back to local bps by uri+line
|
|
254
|
+
// Match server results back to local bps by uri+line.
|
|
255
|
+
// ADT may return a different canonical URI than what was sent (e.g. it rewrites
|
|
256
|
+
// /functions/groups/<g>/includes/<inc>/... to /functions/groups/<g>/fmodules/<fm>/...).
|
|
257
|
+
// Fall back to matching by line alone when URI doesn't match, then adopt the
|
|
258
|
+
// server's canonical URI so future refreshes continue to work.
|
|
231
259
|
const valid = [];
|
|
232
260
|
const stale = [];
|
|
233
261
|
for (const bp of bps) {
|
|
234
|
-
|
|
262
|
+
let match = serverResults.find(r => r.uri === bp.uri && r.line === bp.line);
|
|
263
|
+
if (!match) {
|
|
264
|
+
// Fallback: match by line number alone (handles URI canonicalization by ADT)
|
|
265
|
+
match = serverResults.find(r => r.line === bp.line);
|
|
266
|
+
}
|
|
235
267
|
if (match && match.error) {
|
|
236
268
|
stale.push({ ...bp, error: match.error });
|
|
237
269
|
} else if (match && match.id) {
|
|
238
|
-
|
|
270
|
+
// Adopt the server's canonical URI so subsequent refreshes match correctly
|
|
271
|
+
const canonicalUri = match.uri || bp.uri;
|
|
272
|
+
valid.push({ ...bp, id: match.id, uri: canonicalUri });
|
|
239
273
|
} else {
|
|
240
274
|
// No match in response — server silently dropped it (e.g. expired)
|
|
241
275
|
stale.push({ ...bp, error: 'Not registered on server' });
|
|
@@ -389,9 +423,16 @@ async function cmdSet(args, config, adt) {
|
|
|
389
423
|
}
|
|
390
424
|
|
|
391
425
|
// Update local state with server-assigned IDs
|
|
426
|
+
// Use URI+line match first; fall back to line-only for FUGR where ADT rewrites
|
|
427
|
+
// /includes/<inc>/ to /fmodules/<fm>/ in the response.
|
|
392
428
|
const updatedWithServerIds = updated.map(bp => {
|
|
393
|
-
const sr = serverResults.find(r => r.uri === bp.uri && r.line === bp.line)
|
|
394
|
-
|
|
429
|
+
const sr = serverResults.find(r => r.uri === bp.uri && r.line === bp.line)
|
|
430
|
+
|| serverResults.find(r => r.line === bp.line);
|
|
431
|
+
if (sr && sr.id) {
|
|
432
|
+
const canonicalUri = sr.uri || bp.uri;
|
|
433
|
+
return { ...bp, id: sr.id, uri: canonicalUri };
|
|
434
|
+
}
|
|
435
|
+
return bp;
|
|
395
436
|
});
|
|
396
437
|
if (_saveBpState) _saveBpState(config, updatedWithServerIds);
|
|
397
438
|
|
|
@@ -426,7 +467,8 @@ async function cmdSet(args, config, adt) {
|
|
|
426
467
|
|
|
427
468
|
if (jsonOutput) {
|
|
428
469
|
const out = added.map(a => {
|
|
429
|
-
const sr = serverResults.find(r => r.uri === a.uri && r.line === a.line)
|
|
470
|
+
const sr = serverResults.find(r => r.uri === a.uri && r.line === a.line)
|
|
471
|
+
|| serverResults.find(r => r.line === a.line);
|
|
430
472
|
return { id: (sr && sr.id) || null, object: a.name, line: a.line };
|
|
431
473
|
});
|
|
432
474
|
console.log(JSON.stringify(out.length === 1 ? out[0] : out));
|
package/src/commands/view.js
CHANGED
|
@@ -154,7 +154,9 @@ function findFirstExecutableLine(lines) {
|
|
|
154
154
|
const declPattern = /^\s*(data|final|types|constants|class-data)[\s:(]/i;
|
|
155
155
|
const methodPattern = /^\s*method\s+/i;
|
|
156
156
|
const commentPattern = /^\s*[*"]/;
|
|
157
|
-
|
|
157
|
+
// Program-level header/declaration keywords that are not executable statements
|
|
158
|
+
const progDeclPattern = /^\s*(report|program|parameters|tables|selection-screen|select-options|class-pool|function-pool|interface-pool|type-pool|include)\b/i;
|
|
159
|
+
let inDeclBlock = false; // true while inside a multi-line DATA:/TYPES:/PARAMETERS: block
|
|
158
160
|
for (let i = 0; i < lines.length; i++) {
|
|
159
161
|
const trimmed = lines[i].trim();
|
|
160
162
|
if (inDeclBlock) {
|
|
@@ -170,6 +172,11 @@ function findFirstExecutableLine(lines) {
|
|
|
170
172
|
if (!trimmed.endsWith('.')) inDeclBlock = true;
|
|
171
173
|
continue;
|
|
172
174
|
}
|
|
175
|
+
if (progDeclPattern.test(trimmed)) {
|
|
176
|
+
// Multi-line block (PARAMETERS: ...,\n ...) stays open until period
|
|
177
|
+
if (!trimmed.endsWith('.')) inDeclBlock = true;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
173
180
|
return i;
|
|
174
181
|
}
|
|
175
182
|
return 0;
|
|
@@ -201,6 +208,15 @@ module.exports = {
|
|
|
201
208
|
const jsonOutput = args.includes('--json');
|
|
202
209
|
const fullMode = args.includes('--full');
|
|
203
210
|
const linesMode = args.includes('--lines');
|
|
211
|
+
const fmArgIndex = args.indexOf('--fm');
|
|
212
|
+
const fmName = fmArgIndex !== -1 && fmArgIndex + 1 < args.length
|
|
213
|
+
? args[fmArgIndex + 1].toUpperCase()
|
|
214
|
+
: null;
|
|
215
|
+
|
|
216
|
+
if (fmName && !fullMode) {
|
|
217
|
+
console.error(' Error: --fm requires --full');
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
204
220
|
|
|
205
221
|
console.log(`\n Viewing ${objects.length} object(s)`);
|
|
206
222
|
|
|
@@ -220,6 +236,10 @@ module.exports = {
|
|
|
220
236
|
data.full = true;
|
|
221
237
|
}
|
|
222
238
|
|
|
239
|
+
if (fmName) {
|
|
240
|
+
data.fm = fmName;
|
|
241
|
+
}
|
|
242
|
+
|
|
223
243
|
const result = await http.post('/sap/bc/z_abapgit_agent/view', data, { csrfToken });
|
|
224
244
|
|
|
225
245
|
// Handle uppercase keys from ABAP
|
|
@@ -290,6 +310,7 @@ module.exports = {
|
|
|
290
310
|
const file = section.FILE || section.file || '';
|
|
291
311
|
const lines = section.LINES || section.lines || [];
|
|
292
312
|
const isCmSection = suffix.startsWith('CM') && methodName;
|
|
313
|
+
const isFugrFmSection = !isCmSection && !!methodName;
|
|
293
314
|
|
|
294
315
|
if (linesMode) {
|
|
295
316
|
// --full --lines: dual line numbers (G [N]) for debugging
|
|
@@ -315,10 +336,44 @@ module.exports = {
|
|
|
315
336
|
bpHint = `debug set --objects ${objName}:<global_line>`;
|
|
316
337
|
}
|
|
317
338
|
console.log(` * ---- Method: ${methodName} (${suffix}) — breakpoint: ${bpHint} ----`);
|
|
339
|
+
} else if (isFugrFmSection) {
|
|
340
|
+
// Find first executable line: skip FUNCTION header, comments, blanks,
|
|
341
|
+
// and declaration blocks (DATA:, CONSTANTS:, TYPES:, etc.)
|
|
342
|
+
const declPat = /^\s*(data|final|types|constants|class-data)[\s:(]/i;
|
|
343
|
+
let firstExecLine = 1;
|
|
344
|
+
let inDecl = false;
|
|
345
|
+
for (let li = 0; li < lines.length; li++) {
|
|
346
|
+
const t = lines[li].trim();
|
|
347
|
+
if (inDecl) {
|
|
348
|
+
if (t.endsWith('.')) inDecl = false;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (!t) continue;
|
|
352
|
+
if (/^\*/.test(t)) continue;
|
|
353
|
+
if (/^"/.test(t)) continue;
|
|
354
|
+
if (/^function\s+/i.test(t)) continue;
|
|
355
|
+
if (declPat.test(t)) {
|
|
356
|
+
if (!t.endsWith('.')) inDecl = true;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
firstExecLine = li + 1;
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
const bpHint = `debug set --objects ${suffix}:${firstExecLine}`;
|
|
363
|
+
console.log(` * ---- FM: ${methodName} (${suffix}) — breakpoint: ${bpHint} ----`);
|
|
318
364
|
} else if (file) {
|
|
319
365
|
console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (from .clas.${file}.abap) ----`);
|
|
320
366
|
} else if (suffix) {
|
|
321
|
-
|
|
367
|
+
// For program source sections, emit a breakpoint hint at the first executable line.
|
|
368
|
+
const isProgSection = suffix === 'PROG' || suffix === 'prog';
|
|
369
|
+
if (isProgSection) {
|
|
370
|
+
const execOffset = findFirstExecutableLine(lines);
|
|
371
|
+
const execLine = execOffset + 1; // 1-based
|
|
372
|
+
const bpHint = `debug set --objects ${objName}:${execLine}`;
|
|
373
|
+
console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (${suffix}) — breakpoint: ${bpHint} ----`);
|
|
374
|
+
} else {
|
|
375
|
+
console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (${suffix}) ----`);
|
|
376
|
+
}
|
|
322
377
|
}
|
|
323
378
|
|
|
324
379
|
let includeRelLine = 0;
|
|
@@ -334,6 +389,10 @@ module.exports = {
|
|
|
334
389
|
const gStr = globalLine ? String(globalLine).padStart(4) : ' ';
|
|
335
390
|
const iStr = String(includeRelLine).padStart(3);
|
|
336
391
|
console.log(` ${gStr} [${iStr}] ${codeLine}`);
|
|
392
|
+
} else if (isFugrFmSection) {
|
|
393
|
+
// FM include: line numbers are include-relative = ADT line numbers
|
|
394
|
+
const lStr = String(includeRelLine).padStart(4);
|
|
395
|
+
console.log(` ${lStr} ${codeLine}`);
|
|
337
396
|
} else {
|
|
338
397
|
// For sub-include sections with a known ADT include type,
|
|
339
398
|
// detect METHOD..ENDMETHOD blocks and emit breakpoint hints.
|
|
@@ -363,6 +422,8 @@ module.exports = {
|
|
|
363
422
|
// --full (no --lines): clean source, no line numbers
|
|
364
423
|
if (isCmSection) {
|
|
365
424
|
console.log(` * ---- Method: ${methodName} (${suffix}) ----`);
|
|
425
|
+
} else if (isFugrFmSection) {
|
|
426
|
+
console.log(` * ---- FM: ${methodName} (${suffix}) ----`);
|
|
366
427
|
} else if (file) {
|
|
367
428
|
console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (from .clas.${file}.abap) ----`);
|
|
368
429
|
} else if (suffix) {
|
|
@@ -515,6 +515,8 @@ async function getTopic(topic) {
|
|
|
515
515
|
// Map topic to local guideline file
|
|
516
516
|
const guidelineMap = {
|
|
517
517
|
'abapgit': 'abapgit.md',
|
|
518
|
+
'abapgit-xml-only': 'abapgit-xml-only.md',
|
|
519
|
+
'abapgit-fugr': 'abapgit-fugr.md',
|
|
518
520
|
'xml': 'objects.md',
|
|
519
521
|
'objects': 'objects.md',
|
|
520
522
|
'naming': 'objects.md'
|