abapgit-agent 1.14.4 → 1.15.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.
@@ -60,6 +60,23 @@ function hasFlag(args, flag) {
60
60
  return args.includes(flag);
61
61
  }
62
62
 
63
+ /**
64
+ * Valid class include types for --include flag.
65
+ * User-facing names mirror the abapGit file suffixes (.clas.<name>.abap).
66
+ * Maps user-facing name → ADT /includes/<type> path segment.
67
+ * Verified by live ADT testing: breakpoints accepted for all three.
68
+ * testclasses → testclasses → CCAU (unit test class file)
69
+ * locals_imp → implementations → CCIMP (local class implementations)
70
+ * locals_def → definitions → CCDEF (local class definitions)
71
+ */
72
+ const CLASS_INCLUDE_TYPES = new Set(['testclasses', 'locals_imp', 'locals_def']);
73
+
74
+ const INCLUDE_TYPE_TO_ADT = {
75
+ testclasses: 'testclasses',
76
+ locals_imp: 'implementations',
77
+ locals_def: 'definitions',
78
+ };
79
+
63
80
  /**
64
81
  * Determine ADT object URI from object name (class/interface vs program vs include).
65
82
  * Must use /source/main suffix for classes — verified by live testing: ADT
@@ -71,13 +88,30 @@ function hasFlag(args, flag) {
71
88
  * Class method includes are named <ClassName padded to 30 chars with '='>CM<suffix>
72
89
  * e.g. ZCL_ABGAGT_AGENT=============CM00D
73
90
  * These must be routed to the programs/includes ADT endpoint.
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
+ *
96
+ * When includeType is supplied (testclasses|locals_imp|locals_def),
97
+ * the URI targets the sub-include of the class instead of /source/main.
98
+ * Line numbers are then section-local (from the .clas.<file>.abap file).
74
99
  */
75
- function objectUri(name) {
100
+ function objectUri(name, includeType) {
76
101
  const upper = (name || '').toUpperCase();
77
102
  const lower = upper.toLowerCase();
78
103
  if (/^[ZY](CL|IF)_/.test(upper) || /^(ZCL|ZIF|YCL|YIF)/.test(upper)) {
104
+ if (includeType && CLASS_INCLUDE_TYPES.has(includeType)) {
105
+ const adtType = INCLUDE_TYPE_TO_ADT[includeType];
106
+ return `/sap/bc/adt/oo/classes/${lower}/includes/${adtType}`;
107
+ }
79
108
  return `/sap/bc/adt/oo/classes/${lower}/source/main`;
80
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
+ if (/^L/.test(upper)) {
113
+ return `/sap/bc/adt/programs/includes/${lower}`;
114
+ }
81
115
  return `/sap/bc/adt/programs/programs/${lower}`;
82
116
  }
83
117
 
@@ -247,11 +281,18 @@ function parseBreakpointToken(token) {
247
281
  }
248
282
 
249
283
  async function cmdSet(args, config, adt) {
250
- const objectName = val(args, '--object');
251
- const lineRaw = val(args, '--line');
252
- const filesArg = val(args, '--files');
253
- const objectsArg = val(args, '--objects');
254
- const jsonOutput = hasFlag(args, '--json');
284
+ const objectName = val(args, '--object');
285
+ const lineRaw = val(args, '--line');
286
+ const filesArg = val(args, '--files');
287
+ const objectsArg = val(args, '--objects');
288
+ const includeType = val(args, '--include');
289
+ const jsonOutput = hasFlag(args, '--json');
290
+
291
+ // Validate --include if supplied
292
+ if (includeType && !CLASS_INCLUDE_TYPES.has(includeType)) {
293
+ console.error(` Error: --include must be one of: ${[...CLASS_INCLUDE_TYPES].join(', ')}`);
294
+ process.exit(1);
295
+ }
255
296
 
256
297
  // Collect all breakpoints to add from every accepted input form
257
298
  const toAdd = []; // [{ name, line }]
@@ -304,6 +345,7 @@ async function cmdSet(args, config, adt) {
304
345
  console.error(' debug set --files src/zcl_my_class.clas.abap:42');
305
346
  console.error(' debug set --objects ZCL_MY_CLASS:42');
306
347
  console.error(' debug set --object ZCL_MY_CLASS --line 42');
348
+ console.error(' debug set --objects ZCL_MY_CLASS:16 --include testclasses');
307
349
  process.exit(1);
308
350
  }
309
351
 
@@ -312,7 +354,7 @@ async function cmdSet(args, config, adt) {
312
354
  const added = [];
313
355
 
314
356
  for (const { name, line } of toAdd) {
315
- const uri = objectUri(name);
357
+ const uri = objectUri(name, includeType);
316
358
  const objUpper = name.toUpperCase();
317
359
  // Skip if an identical breakpoint already exists
318
360
  if (existing.some(bp => bp.object === objUpper && bp.line === line)) {
@@ -145,18 +145,38 @@ async function computeGlobalStarts(objName, sections, config) {
145
145
 
146
146
  /**
147
147
  * Given the lines of a CM method section, return the 0-based index of the
148
- * first "executable" line — i.e. skip METHOD, blank lines, and pure
149
- * declaration lines (DATA/FINAL/TYPES/CONSTANTS/CLASS-DATA).
148
+ * first "executable" line — i.e. skip METHOD, blank lines, comment lines,
149
+ * and declaration lines (DATA/FINAL/TYPES/CONSTANTS/CLASS-DATA), including
150
+ * multi-line DATA: blocks whose continuation lines end with a period.
150
151
  * Returns 0 if no better line is found (falls back to METHOD statement).
151
152
  */
152
153
  function findFirstExecutableLine(lines) {
153
- const declPattern = /^\s*(data|final|types|constants|class-data)[\s:]/i;
154
+ const declPattern = /^\s*(data|final|types|constants|class-data)[\s:(]/i;
154
155
  const methodPattern = /^\s*method\s+/i;
156
+ const commentPattern = /^\s*[*"]/;
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
155
160
  for (let i = 0; i < lines.length; i++) {
156
161
  const trimmed = lines[i].trim();
162
+ if (inDeclBlock) {
163
+ // continuation line — skip until the block closes with a period
164
+ if (trimmed.endsWith('.')) inDeclBlock = false;
165
+ continue;
166
+ }
157
167
  if (!trimmed) continue; // blank line
158
168
  if (methodPattern.test(trimmed)) continue; // METHOD statement itself
159
- if (declPattern.test(trimmed)) continue; // declaration
169
+ if (commentPattern.test(trimmed)) continue; // comment line
170
+ if (declPattern.test(trimmed)) {
171
+ // Multi-line block (DATA: ...,\n ...) stays open until period
172
+ if (!trimmed.endsWith('.')) inDeclBlock = true;
173
+ continue;
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
+ }
160
180
  return i;
161
181
  }
162
182
  return 0;
@@ -188,6 +208,15 @@ module.exports = {
188
208
  const jsonOutput = args.includes('--json');
189
209
  const fullMode = args.includes('--full');
190
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
+ }
191
220
 
192
221
  console.log(`\n Viewing ${objects.length} object(s)`);
193
222
 
@@ -207,6 +236,10 @@ module.exports = {
207
236
  data.full = true;
208
237
  }
209
238
 
239
+ if (fmName) {
240
+ data.fm = fmName;
241
+ }
242
+
210
243
  const result = await http.post('/sap/bc/z_abapgit_agent/view', data, { csrfToken });
211
244
 
212
245
  // Handle uppercase keys from ABAP
@@ -277,11 +310,22 @@ module.exports = {
277
310
  const file = section.FILE || section.file || '';
278
311
  const lines = section.LINES || section.lines || [];
279
312
  const isCmSection = suffix.startsWith('CM') && methodName;
313
+ const isFugrFmSection = !isCmSection && !!methodName;
280
314
 
281
315
  if (linesMode) {
282
316
  // --full --lines: dual line numbers (G [N]) for debugging
283
317
  const globalStart = section.globalStart || 0;
284
318
 
319
+ // Map abapGit file suffix to the --include flag value used in debug set hints.
320
+ // User-facing names mirror the abapGit file suffixes (.clas.<name>.abap).
321
+ // Verified by live ADT testing: /includes/<adtType> endpoint accepts BPs.
322
+ const INCLUDE_FLAG_VALUE = {
323
+ testclasses: 'testclasses',
324
+ locals_imp: 'locals_imp',
325
+ locals_def: 'locals_def',
326
+ };
327
+ const includeFlag = INCLUDE_FLAG_VALUE[file] || null;
328
+
285
329
  if (isCmSection) {
286
330
  let bpHint;
287
331
  if (globalStart) {
@@ -292,13 +336,52 @@ module.exports = {
292
336
  bpHint = `debug set --objects ${objName}:<global_line>`;
293
337
  }
294
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} ----`);
295
364
  } else if (file) {
296
365
  console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (from .clas.${file}.abap) ----`);
297
366
  } else if (suffix) {
298
- console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (${suffix}) ----`);
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
+ }
299
377
  }
300
378
 
301
379
  let includeRelLine = 0;
380
+ // Track when we're inside a METHOD block in a sub-include section
381
+ // so we can emit a breakpoint hint at the first executable line.
382
+ let inSubMethod = false;
383
+ let subMethodName = '';
384
+ let subMethodStartLine = 0; // 1-based line of METHOD statement
302
385
  for (const codeLine of lines) {
303
386
  includeRelLine++;
304
387
  const globalLine = globalStart ? globalStart + includeRelLine - 1 : 0;
@@ -306,8 +389,32 @@ module.exports = {
306
389
  const gStr = globalLine ? String(globalLine).padStart(4) : ' ';
307
390
  const iStr = String(includeRelLine).padStart(3);
308
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}`);
309
396
  } else {
310
- const lStr = globalLine ? String(globalLine).padStart(4) : String(includeRelLine).padStart(4);
397
+ // For sub-include sections with a known ADT include type,
398
+ // detect METHOD..ENDMETHOD blocks and emit breakpoint hints.
399
+ if (includeFlag) {
400
+ const trimmed = codeLine.trim();
401
+ if (!inSubMethod && /^method\s+/i.test(trimmed)) {
402
+ // Entering a new method — find first executable line offset
403
+ // by scanning ahead from this line
404
+ const mName = (trimmed.match(/^method\s+([\w~]+)/i) || [])[1] || '';
405
+ // Collect lines from this METHOD onwards to find exec offset
406
+ const remainingLines = lines.slice(includeRelLine - 1); // 0-based from current
407
+ const execOffset = findFirstExecutableLine(remainingLines);
408
+ const execLine = includeRelLine + execOffset; // section-local line
409
+ const bpHint = `debug set --objects ${objName}:${execLine} --include ${includeFlag}`;
410
+ console.log(` * ---- Method: ${mName.toUpperCase()} — breakpoint: ${bpHint} ----`);
411
+ inSubMethod = true;
412
+ subMethodName = mName;
413
+ } else if (inSubMethod && /^endmethod\s*\./i.test(codeLine.trim())) {
414
+ inSubMethod = false;
415
+ }
416
+ }
417
+ const lStr = String(includeRelLine).padStart(4);
311
418
  console.log(` ${lStr} ${codeLine}`);
312
419
  }
313
420
  }
@@ -315,6 +422,8 @@ module.exports = {
315
422
  // --full (no --lines): clean source, no line numbers
316
423
  if (isCmSection) {
317
424
  console.log(` * ---- Method: ${methodName} (${suffix}) ----`);
425
+ } else if (isFugrFmSection) {
426
+ console.log(` * ---- FM: ${methodName} (${suffix}) ----`);
318
427
  } else if (file) {
319
428
  console.log(` * ---- Section: ${section.DESCRIPTION || section.description} (from .clas.${file}.abap) ----`);
320
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'