abapgit-agent 1.18.1 → 1.19.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/abap/CLAUDE.md +2 -3
- package/abap/guidelines/abapgit-xml-only.md +63 -3
- package/abap/guidelines/debug-session.md +2 -3
- package/package.json +2 -1
- package/src/commands/debug.js +23 -40
- package/src/commands/inspect.js +174 -70
- package/src/commands/pull.js +44 -1
- package/src/commands/unit.js +290 -200
- package/src/commands/view.js +46 -1
- package/src/utils/adt-http.js +5 -2
package/abap/CLAUDE.md
CHANGED
|
@@ -511,8 +511,7 @@ Never assume — wait for the user's answer before proceeding.
|
|
|
511
511
|
1. **Always use `--json`** for ALL debug commands (`attach`, `vars`, `stack`, `step`) — human output is not machine-parseable. This is non-negotiable.
|
|
512
512
|
2. **Attach BEFORE trigger** — start `debug attach --json` in background first, wait for `"Listener active"`, THEN fire the trigger (`unit`/`pull`/`run`)
|
|
513
513
|
3. **Never pull to trigger** if a simpler trigger works — use `unit` when a test exists, `run` for a class runner; use `pull` only when the bug is specifically in the pull flow
|
|
514
|
-
4. **
|
|
515
|
-
5. **Always finish with `step --type continue --json`** — releases the frozen ABAP work process
|
|
514
|
+
4. **Always finish with `step --type continue --json`** — releases the frozen ABAP work process
|
|
516
515
|
|
|
517
516
|
**Finding the right line number for a breakpoint:**
|
|
518
517
|
|
|
@@ -700,7 +699,7 @@ abapgit-agent pull --files src/<name>.clas.abap --sync-xml # --sync-xml amends
|
|
|
700
699
|
```
|
|
701
700
|
|
|
702
701
|
<!-- AI-CONDENSED-START -->
|
|
703
|
-
# AI Agent
|
|
702
|
+
# AI Agent Quick Reference
|
|
704
703
|
|
|
705
704
|
## Step 1: Read project config before doing anything
|
|
706
705
|
|
|
@@ -6,9 +6,9 @@ parent: ABAP Coding Guidelines
|
|
|
6
6
|
grand_parent: ABAP Development
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
# abapGit XML Metadata — XML-Only Objects (TABL, STRU, DTEL, TTYP, DOMA, MSAG)
|
|
9
|
+
# abapGit XML Metadata — XML-Only Objects (TABL, STRU, DTEL, TTYP, DOMA, MSAG, SUSO)
|
|
10
10
|
|
|
11
|
-
XML templates for ABAP Dictionary objects that have no `.abap` source file — TABL, STRU, DTEL, TTYP, DOMA,
|
|
11
|
+
XML templates for ABAP Dictionary objects that have no `.abap` source file — TABL, STRU, DTEL, TTYP, DOMA, message classes (MSAG), and authorization objects (SUSO).
|
|
12
12
|
|
|
13
13
|
> **CRITICAL: Always write XML files with a UTF-8 BOM (`\ufeff`) as the very first character**, before `<?xml ...`.
|
|
14
14
|
> Without the BOM, abapGit shows the object as **"M" (modified)** after every pull.
|
|
@@ -19,7 +19,7 @@ XML templates for ABAP Dictionary objects that have no `.abap` source file — T
|
|
|
19
19
|
> **For CLAS, INTF, PROG, DDLS, DCLS, FUGR** (have source files): see `abapgit-agent ref --topic abapgit`
|
|
20
20
|
> **For DDLS, DCLS** (CDS — have source files): also see `abapgit-agent ref --topic abapgit`
|
|
21
21
|
|
|
22
|
-
**Searchable keywords**: table xml, structure xml, data element xml, table type xml, domain xml, message class xml, tabl, stru, dtel, ttyp, doma, msag, dictionary
|
|
22
|
+
**Searchable keywords**: table xml, structure xml, data element xml, table type xml, domain xml, message class xml, tabl, stru, dtel, ttyp, doma, msag, suso, authorization object, dictionary
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
@@ -451,3 +451,63 @@ abapGit uses the view `DD01V` as the root element (not `DD01L`). The serializer
|
|
|
451
451
|
- `T100/TEXT`: Message text — use `&1`–`&4` for placeholders (serialized as `&1`–`&4` in XML)
|
|
452
452
|
|
|
453
453
|
**Note**: The `<T100>` wrapper contains repeated `<T100>` child elements — one per message.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
### Authorization Object (SUSO)
|
|
458
|
+
|
|
459
|
+
**Filename**: `src/zmy_suso.suso.xml`
|
|
460
|
+
|
|
461
|
+
> Pull with: `pull --files src/zmy_suso.suso.xml`
|
|
462
|
+
>
|
|
463
|
+
> View with: `view --objects ZMY_SUSO --type SUSO`
|
|
464
|
+
|
|
465
|
+
```xml
|
|
466
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
467
|
+
<abapGit version="v1.0.0" serializer="LCL_OBJECT_SUSO" serializer_version="v1.0.0">
|
|
468
|
+
<asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
|
|
469
|
+
<asx:values>
|
|
470
|
+
<TOBJ>
|
|
471
|
+
<OBJCT>ZMY_SUSO</OBJCT>
|
|
472
|
+
<FIEL1>ACTVT</FIEL1>
|
|
473
|
+
<FIEL2>ZMY_FIELD</FIEL2>
|
|
474
|
+
<OCLSS>ZMY</OCLSS>
|
|
475
|
+
</TOBJ>
|
|
476
|
+
<TOBJT>
|
|
477
|
+
<LANGU>E</LANGU>
|
|
478
|
+
<OBJECT>ZMY_SUSO</OBJECT>
|
|
479
|
+
<TTEXT>My authorization object description</TTEXT>
|
|
480
|
+
</TOBJT>
|
|
481
|
+
<TOBJVORFLG>
|
|
482
|
+
<OBJCT>ZMY_SUSO</OBJCT>
|
|
483
|
+
</TOBJVORFLG>
|
|
484
|
+
<TACTZ>
|
|
485
|
+
<TACTZ>
|
|
486
|
+
<BROBJ>ZMY_SUSO</BROBJ>
|
|
487
|
+
<ACTVT>01</ACTVT>
|
|
488
|
+
</TACTZ>
|
|
489
|
+
<TACTZ>
|
|
490
|
+
<BROBJ>ZMY_SUSO</BROBJ>
|
|
491
|
+
<ACTVT>02</ACTVT>
|
|
492
|
+
</TACTZ>
|
|
493
|
+
<TACTZ>
|
|
494
|
+
<BROBJ>ZMY_SUSO</BROBJ>
|
|
495
|
+
<ACTVT>03</ACTVT>
|
|
496
|
+
</TACTZ>
|
|
497
|
+
</TACTZ>
|
|
498
|
+
</asx:values>
|
|
499
|
+
</asx:abap>
|
|
500
|
+
</abapGit>
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Key Fields**:
|
|
504
|
+
- `TOBJ/OBJCT`: Authorization object name — same as the filename stem
|
|
505
|
+
- `TOBJ/FIEL1`–`FIEL0`: Up to 10 authorization fields (from SU20) that make up this object
|
|
506
|
+
- `TOBJ/OCLSS`: Object class (4 chars) — groups related authorization objects (e.g. `AAAB`, `BC_A`)
|
|
507
|
+
- `TOBJT/TTEXT`: Description text
|
|
508
|
+
- `TOBJVORFLG`: Flags record — include with just `OBJCT` when no flags are set
|
|
509
|
+
- `TACTZ`: Allowed activity values — one `<TACTZ>` entry per permitted `ACTVT` value
|
|
510
|
+
|
|
511
|
+
**Note**: Authorization objects (SUSO) are the SU21 objects that group authorization fields (AUTH/SU20)
|
|
512
|
+
together. An object like `AUD_SCOPEM` uses fields `ACTVT`, `BO_SERVICE`, and `AUD_GROUP`.
|
|
513
|
+
|
|
@@ -148,7 +148,7 @@ abapgit-agent debug list # confirm it was registered
|
|
|
148
148
|
|
|
149
149
|
Best practice: individual sequential calls. Once the daemon is running and
|
|
150
150
|
the session is saved to the state file, each `vars/stack/step` command is a
|
|
151
|
-
plain standalone call —
|
|
151
|
+
plain standalone call — the session is loaded automatically.
|
|
152
152
|
|
|
153
153
|
```bash
|
|
154
154
|
# Start attach listener in background (spawns a daemon, saves session to state file)
|
|
@@ -183,7 +183,7 @@ done
|
|
|
183
183
|
# "position" contains "line" and "isActive":true. If position stays empty after the
|
|
184
184
|
# trigger finishes, the breakpoint was missed — re-delete, re-set, re-attach, re-trigger.
|
|
185
185
|
|
|
186
|
-
# Inspect and step — each is an individual call,
|
|
186
|
+
# Inspect and step — each is an individual call, session loaded automatically
|
|
187
187
|
abapgit-agent debug stack --json
|
|
188
188
|
abapgit-agent debug vars --json
|
|
189
189
|
abapgit-agent debug vars --expand LS_OBJECT --json
|
|
@@ -202,7 +202,6 @@ rm -f /tmp/attach.json /tmp/trigger.json
|
|
|
202
202
|
> 1. Wait for `"Listener active"` in the attach output before firing the trigger — this message is printed to stderr (captured in `attach.json`) the moment the listener POST is about to reach ADT. A blind `sleep` is not reliable under system load.
|
|
203
203
|
> 2. **Always run the trigger in background (`&`)** — whether using `unit` or `run --class`. A foreground trigger blocks the terminal; if it is interrupted before the debug session completes, the ABAP work process is left frozen. The trigger must stay alive for the entire session.
|
|
204
204
|
> 3. Always finish with `step --type continue` — this releases the frozen work process so the trigger can complete normally
|
|
205
|
-
> 4. **Never pass `--session` to `step/vars/stack`** — it bypasses the daemon IPC and causes `noSessionAttached`. Omit it and let commands auto-load from the saved state file.
|
|
206
205
|
|
|
207
206
|
**Step 3 — step through and inspect**
|
|
208
207
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abapgit-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
|
|
5
5
|
"files": [
|
|
6
6
|
"bin/",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"test:pull": "node tests/run-all.js --pull",
|
|
55
55
|
"test:full-pull": "node tests/run-all.js --full-pull",
|
|
56
56
|
"test:conflict": "node tests/run-all.js --conflict",
|
|
57
|
+
"test:async-pull": "node tests/run-all.js --async-pull",
|
|
57
58
|
"pull": "node bin/abapgit-agent",
|
|
58
59
|
"release": "node scripts/release.js",
|
|
59
60
|
"unrelease": "node scripts/unrelease.js"
|
package/src/commands/debug.js
CHANGED
|
@@ -637,7 +637,6 @@ async function cmdDelete(args, config, adt) {
|
|
|
637
637
|
// ─── Sub-command: attach ──────────────────────────────────────────────────────
|
|
638
638
|
|
|
639
639
|
async function cmdAttach(args, config, adt) {
|
|
640
|
-
const sessionIdOverride = val(args, '--session');
|
|
641
640
|
const jsonOutput = hasFlag(args, '--json');
|
|
642
641
|
const pollTimeout = parseInt(val(args, '--timeout') || '30', 10);
|
|
643
642
|
const maxListenSeconds = parseInt(val(args, '--max-listen') || '240', 10);
|
|
@@ -653,9 +652,8 @@ async function cmdAttach(args, config, adt) {
|
|
|
653
652
|
process.stderr.write('\n Waiting for breakpoint hit... (run your ABAP program in a separate window)\n');
|
|
654
653
|
}
|
|
655
654
|
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
// by debug-session.js STATEFUL_HEADER on attach/getStack/step/vars.
|
|
655
|
+
// Refresh session for the attach flow. Destroy the keepAlive agent to get
|
|
656
|
+
// a fresh TCP connection (old connections may carry stale server-side state).
|
|
659
657
|
adt.clearSession();
|
|
660
658
|
if (adt._axios) {
|
|
661
659
|
const agent = adt._axios.defaults.httpsAgent || adt._axios.defaults.httpAgent;
|
|
@@ -669,30 +667,25 @@ async function cmdAttach(args, config, adt) {
|
|
|
669
667
|
await adt.fetchCsrfToken();
|
|
670
668
|
|
|
671
669
|
// Delete any stale listener registered under our terminalId from a previous
|
|
672
|
-
// (possibly crashed) run.
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
await adt.delete(delUrl);
|
|
686
|
-
if (jsonOutput) process.stderr.write(`[debug-attach] deleted stale listener\n`);
|
|
687
|
-
} catch (e) {
|
|
688
|
-
if (jsonOutput) process.stderr.write(`[debug-attach] delete listener (cleanup): ${e.statusCode || e.message}\n`);
|
|
689
|
-
}
|
|
670
|
+
// (possibly crashed) run.
|
|
671
|
+
try {
|
|
672
|
+
const delUrl = `/sap/bc/adt/debugger/listeners` +
|
|
673
|
+
`?debuggingMode=user` +
|
|
674
|
+
`&requestUser=${encodeURIComponent((config.user || '').toUpperCase())}` +
|
|
675
|
+
`&terminalId=${encodeURIComponent(ADT_CLIENT_ID)}` +
|
|
676
|
+
`&ideId=${encodeURIComponent(ADT_CLIENT_ID)}` +
|
|
677
|
+
`&checkConflict=false` +
|
|
678
|
+
`¬ifyConflict=true`;
|
|
679
|
+
await adt.delete(delUrl);
|
|
680
|
+
if (jsonOutput) process.stderr.write(`[debug-attach] deleted stale listener\n`);
|
|
681
|
+
} catch (e) {
|
|
682
|
+
if (jsonOutput) process.stderr.write(`[debug-attach] delete listener (cleanup): ${e.statusCode || e.message}\n`);
|
|
690
683
|
}
|
|
691
684
|
|
|
692
685
|
// Re-POST local breakpoints to refresh them on the server before listening.
|
|
693
686
|
// Breakpoints expire when the SAP session or work process is restarted.
|
|
694
687
|
// This ensures they are active regardless of when they were originally set.
|
|
695
|
-
|
|
688
|
+
{
|
|
696
689
|
const localBps = _loadBpState ? _loadBpState(config) : [];
|
|
697
690
|
if (localBps.length === 0) {
|
|
698
691
|
console.error('\n No breakpoints set. Use "debug set --object <name> --line <n>" first.\n');
|
|
@@ -700,6 +693,9 @@ async function cmdAttach(args, config, adt) {
|
|
|
700
693
|
}
|
|
701
694
|
const { valid, stale } = await refreshBreakpoints(config, adt, localBps);
|
|
702
695
|
if (_saveBpState) _saveBpState(config, valid);
|
|
696
|
+
if (jsonOutput && valid.length > 0) {
|
|
697
|
+
process.stderr.write(`[debug-attach] refreshed ${valid.length} breakpoint(s)\n`);
|
|
698
|
+
}
|
|
703
699
|
if (stale.length > 0 && !jsonOutput) {
|
|
704
700
|
process.stderr.write(` Warning: ${stale.length} breakpoint(s) could not be registered (invalid position):\n`);
|
|
705
701
|
stale.forEach(({ object, line, error }) => {
|
|
@@ -711,11 +707,11 @@ async function cmdAttach(args, config, adt) {
|
|
|
711
707
|
process.exit(1);
|
|
712
708
|
}
|
|
713
709
|
}
|
|
714
|
-
let sessionId =
|
|
710
|
+
let sessionId = null;
|
|
715
711
|
let positionResult = null;
|
|
716
712
|
let session = null;
|
|
717
713
|
|
|
718
|
-
|
|
714
|
+
{
|
|
719
715
|
// ADT listeners: POST long-polls until a breakpoint is hit (or timeout).
|
|
720
716
|
// On breakpoint hit the response body contains <DEBUGGEE_ID>; on timeout
|
|
721
717
|
// (no breakpoint) returns 200 with empty body — poll again.
|
|
@@ -845,8 +841,8 @@ async function cmdAttach(args, config, adt) {
|
|
|
845
841
|
process.exit(1);
|
|
846
842
|
}
|
|
847
843
|
// attach() succeeded — skip getPosition() validation.
|
|
848
|
-
//
|
|
849
|
-
//
|
|
844
|
+
// The in-process daemon shares the same AdtHttp instance, so
|
|
845
|
+
// getStack/vars/step via IPC use the identical TCP connection.
|
|
850
846
|
positionResult = { position: {}, source: [] };
|
|
851
847
|
session = candidateSession;
|
|
852
848
|
}
|
|
@@ -881,20 +877,7 @@ async function cmdAttach(args, config, adt) {
|
|
|
881
877
|
}
|
|
882
878
|
}
|
|
883
879
|
|
|
884
|
-
// When using --session override (no listener), do attach+getPosition here.
|
|
885
|
-
if (sessionIdOverride) {
|
|
886
|
-
session = new DebugSession(adt, sessionId);
|
|
887
|
-
// getPosition is best-effort for --session override (e.g. human REPL recovery)
|
|
888
|
-
try {
|
|
889
|
-
positionResult = await session.getPosition();
|
|
890
|
-
} catch (e) {
|
|
891
|
-
positionResult = { position: {}, source: [] };
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// session and positionResult are now set (either via listener loop or --session override)
|
|
896
880
|
if (!session) {
|
|
897
|
-
// Should not reach here — listener loop either sets session or exits
|
|
898
881
|
console.error('\n Internal error: session not initialized after attach.\n');
|
|
899
882
|
process.exit(1);
|
|
900
883
|
}
|
package/src/commands/inspect.js
CHANGED
|
@@ -24,15 +24,17 @@ function escapeXml(str) {
|
|
|
24
24
|
* Maps to JUnit schema:
|
|
25
25
|
* <testsuites>
|
|
26
26
|
* <testsuite name="CLAS ZCL_MY_CLASS" tests="N" failures="F" errors="0">
|
|
27
|
-
* <testcase name="Syntax check" classname="ZCL_MY_CLASS"
|
|
28
|
-
*
|
|
27
|
+
* <testcase name="Inspect / Syntax check" classname="ZCL_MY_CLASS"/> ← clean
|
|
28
|
+
* <testcase name="Inspect / CL_CI_TEST_EXCEPTION/UNHANDLED_01" ← finding
|
|
29
|
+
* classname="ZCL_MY_CLASS">
|
|
30
|
+
* <failure type="SyntaxError" message="...">method/line/detail</failure>
|
|
29
31
|
* </testcase>
|
|
30
32
|
* </testsuite>
|
|
31
33
|
* </testsuites>
|
|
32
34
|
*
|
|
33
|
-
* One testsuite per object. Each error becomes
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* One testsuite per object. Each error/warning becomes its own testcase named
|
|
36
|
+
* after the SCI check rule (check_class/check_code) so Jenkins shows exactly
|
|
37
|
+
* which rule fired. Clean objects get a single passing testcase.
|
|
36
38
|
*/
|
|
37
39
|
function buildInspectJUnit(results) {
|
|
38
40
|
const suites = results.map(res => {
|
|
@@ -42,28 +44,30 @@ function buildInspectJUnit(results) {
|
|
|
42
44
|
const warnings = res.WARNINGS !== undefined ? res.WARNINGS : (res.warnings || []);
|
|
43
45
|
const errorCount = errors.length;
|
|
44
46
|
const warnCount = warnings.length;
|
|
45
|
-
// One testcase per error/warning; at least one testcase for a clean object
|
|
46
47
|
const testCount = Math.max(1, errorCount + warnCount);
|
|
47
48
|
|
|
48
49
|
const testcases = [];
|
|
49
50
|
|
|
50
51
|
if (errorCount === 0 && warnCount === 0) {
|
|
51
|
-
testcases.push(` <testcase name="Syntax check" classname="${escapeXml(objectName)}"/>`);
|
|
52
|
+
testcases.push(` <testcase name="Inspect / Syntax check" classname="${escapeXml(objectName)}"/>`);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
for (const err of errors) {
|
|
55
|
-
const line = err.LINE
|
|
56
|
-
const column = err.COLUMN
|
|
57
|
-
const text = err.TEXT
|
|
56
|
+
const line = err.LINE || err.line || '?';
|
|
57
|
+
const column = err.COLUMN || err.column || '?';
|
|
58
|
+
const text = err.TEXT || err.text || 'Unknown error';
|
|
58
59
|
const methodName = err.METHOD_NAME || err.method_name;
|
|
59
|
-
const sobjname = err.SOBJNAME
|
|
60
|
+
const sobjname = err.SOBJNAME || err.sobjname || '';
|
|
61
|
+
const checkClass = err.CHECK_CLASS || err.check_class || '';
|
|
62
|
+
const checkCode = err.CHECK_CODE || err.check_code || '';
|
|
60
63
|
const detail = [
|
|
61
64
|
methodName ? `Method: ${methodName}` : null,
|
|
62
65
|
`Line ${line}, Column ${column}`,
|
|
63
66
|
sobjname ? `Include: ${sobjname}` : null,
|
|
64
67
|
text
|
|
65
68
|
].filter(Boolean).join('\n');
|
|
66
|
-
const
|
|
69
|
+
const checkId = checkClass && checkCode ? `${checkClass}/${checkCode}` : null;
|
|
70
|
+
const caseName = checkId ? `Inspect / ${checkId}` : (methodName ? `${methodName} line ${line}` : `Line ${line}`);
|
|
67
71
|
testcases.push(
|
|
68
72
|
` <testcase name="${escapeXml(caseName)}" classname="${escapeXml(objectName)}">\n` +
|
|
69
73
|
` <failure type="SyntaxError" message="${escapeXml(text)}">${escapeXml(detail)}</failure>\n` +
|
|
@@ -72,17 +76,20 @@ function buildInspectJUnit(results) {
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
for (const warn of warnings) {
|
|
75
|
-
const line = warn.LINE
|
|
76
|
-
const text = warn.MESSAGE
|
|
79
|
+
const line = warn.LINE || warn.line || '?';
|
|
80
|
+
const text = warn.MESSAGE || warn.message || warn.TEXT || warn.text || 'Warning';
|
|
77
81
|
const methodName = warn.METHOD_NAME || warn.method_name;
|
|
78
|
-
const sobjname = warn.SOBJNAME
|
|
82
|
+
const sobjname = warn.SOBJNAME || warn.sobjname || '';
|
|
83
|
+
const checkClass = warn.CHECK_CLASS || warn.check_class || '';
|
|
84
|
+
const checkCode = warn.CHECK_CODE || warn.check_code || '';
|
|
79
85
|
const detail = [
|
|
80
86
|
methodName ? `Method: ${methodName}` : null,
|
|
81
87
|
`Line ${line}`,
|
|
82
88
|
sobjname ? `Include: ${sobjname}` : null,
|
|
83
89
|
text
|
|
84
90
|
].filter(Boolean).join('\n');
|
|
85
|
-
const
|
|
91
|
+
const checkId = checkClass && checkCode ? `${checkClass}/${checkCode}` : null;
|
|
92
|
+
const caseName = checkId ? `Inspect / ${checkId}` : (methodName ? `${methodName} line ${line}` : `Line ${line}`);
|
|
86
93
|
testcases.push(
|
|
87
94
|
` <testcase name="${escapeXml(caseName)}" classname="${escapeXml(objectName)}">\n` +
|
|
88
95
|
` <failure type="Warning" message="${escapeXml(text)}">${escapeXml(detail)}</failure>\n` +
|
|
@@ -107,8 +114,10 @@ function buildInspectJUnit(results) {
|
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
/**
|
|
110
|
-
* Inspect
|
|
117
|
+
* Inspect a single batch of files in one request (max CHUNK_SIZE files)
|
|
111
118
|
*/
|
|
119
|
+
const CHUNK_SIZE = 10;
|
|
120
|
+
|
|
112
121
|
async function inspectAllFiles(files, csrfToken, config, variant, http, verbose = false) {
|
|
113
122
|
// Convert files to uppercase names
|
|
114
123
|
const fileNames = files.map(f => {
|
|
@@ -152,6 +161,66 @@ async function inspectAllFiles(files, csrfToken, config, variant, http, verbose
|
|
|
152
161
|
}
|
|
153
162
|
}
|
|
154
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Apply inspect.suppress rules to a single result — downgrade matching errors/warnings to infos.
|
|
166
|
+
*/
|
|
167
|
+
function applySuppressRules(result, suppressRules) {
|
|
168
|
+
if (!suppressRules || suppressRules.length === 0) return;
|
|
169
|
+
const objName = (result.OBJECT_NAME || result.object_name || '').toUpperCase();
|
|
170
|
+
for (const rule of suppressRules) {
|
|
171
|
+
const objPattern = new RegExp('^' + (rule.object || '*').toUpperCase().replace(/\*/g, '.*') + '$');
|
|
172
|
+
if (!objPattern.test(objName)) continue;
|
|
173
|
+
const msgPattern = new RegExp((rule.message || '*').replace(/\*/g, '.*'), 'i');
|
|
174
|
+
|
|
175
|
+
const errors = result.ERRORS || result.errors || [];
|
|
176
|
+
const kept = [];
|
|
177
|
+
for (const err of errors) {
|
|
178
|
+
const text = err.TEXT || err.text || '';
|
|
179
|
+
if (msgPattern.test(text)) {
|
|
180
|
+
const infos = result.INFOS || result.infos || [];
|
|
181
|
+
infos.push({ ...err, MESSAGE: `[suppressed] ${text}`, message: `[suppressed] ${text}` });
|
|
182
|
+
if (result.INFOS !== undefined) result.INFOS = infos; else result.infos = infos;
|
|
183
|
+
const ec = result.ERROR_COUNT !== undefined ? 'ERROR_COUNT' : 'error_count';
|
|
184
|
+
result[ec] = Math.max(0, (result[ec] || 0) - 1);
|
|
185
|
+
} else {
|
|
186
|
+
kept.push(err);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (result.ERRORS !== undefined) result.ERRORS = kept; else result.errors = kept;
|
|
190
|
+
|
|
191
|
+
const warnings = result.WARNINGS || result.warnings || [];
|
|
192
|
+
const keptW = [];
|
|
193
|
+
for (const warn of warnings) {
|
|
194
|
+
const text = warn.MESSAGE || warn.message || '';
|
|
195
|
+
if (msgPattern.test(text)) {
|
|
196
|
+
const infos = result.INFOS || result.infos || [];
|
|
197
|
+
infos.push({ ...warn, MESSAGE: `[suppressed] ${text}`, message: `[suppressed] ${text}` });
|
|
198
|
+
if (result.INFOS !== undefined) result.INFOS = infos; else result.infos = infos;
|
|
199
|
+
} else {
|
|
200
|
+
keptW.push(warn);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (result.WARNINGS !== undefined) result.WARNINGS = keptW; else result.warnings = keptW;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Split files into chunks and inspect each sequentially, combining results.
|
|
209
|
+
* Calls onChunkResult(result) for each object result as chunks complete.
|
|
210
|
+
*/
|
|
211
|
+
async function inspectInChunks(files, csrfToken, config, variant, http, verbose = false, onChunkResult = null) {
|
|
212
|
+
const results = [];
|
|
213
|
+
for (let i = 0; i < files.length; i += CHUNK_SIZE) {
|
|
214
|
+
const chunk = files.slice(i, i + CHUNK_SIZE);
|
|
215
|
+
const chunkResults = await inspectAllFiles(chunk, csrfToken, config, variant, http, verbose);
|
|
216
|
+
for (const result of chunkResults) {
|
|
217
|
+
results.push(result);
|
|
218
|
+
if (onChunkResult) onChunkResult(result);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return results;
|
|
222
|
+
}
|
|
223
|
+
|
|
155
224
|
/**
|
|
156
225
|
* Process a single inspect result
|
|
157
226
|
*/
|
|
@@ -181,6 +250,8 @@ async function processInspectResult(res) {
|
|
|
181
250
|
const text = err.TEXT || err.text || 'Unknown error';
|
|
182
251
|
const methodName = err.METHOD_NAME || err.method_name;
|
|
183
252
|
const sobjname = err.SOBJNAME || err.sobjname;
|
|
253
|
+
const checkClass = err.CHECK_CLASS || err.check_class || '';
|
|
254
|
+
const checkCode = err.CHECK_CODE || err.check_code || '';
|
|
184
255
|
|
|
185
256
|
if (methodName) {
|
|
186
257
|
console.log(` Method: ${methodName}`);
|
|
@@ -190,6 +261,10 @@ async function processInspectResult(res) {
|
|
|
190
261
|
console.log(` Include: ${sobjname}`);
|
|
191
262
|
}
|
|
192
263
|
console.log(` ${text}`);
|
|
264
|
+
if (checkCode) {
|
|
265
|
+
console.log(` [${checkClass}/${checkCode}]`);
|
|
266
|
+
console.log(` → abapgit-agent inspect --doc ${checkClass}/${checkCode}`);
|
|
267
|
+
}
|
|
193
268
|
console.log('');
|
|
194
269
|
}
|
|
195
270
|
|
|
@@ -202,6 +277,8 @@ async function processInspectResult(res) {
|
|
|
202
277
|
const text = warn.MESSAGE || warn.message || 'Unknown warning';
|
|
203
278
|
const methodName = warn.METHOD_NAME || warn.method_name;
|
|
204
279
|
const sobjname = warn.SOBJNAME || warn.sobjname;
|
|
280
|
+
const checkClass = warn.CHECK_CLASS || warn.check_class || '';
|
|
281
|
+
const checkCode = warn.CHECK_CODE || warn.check_code || '';
|
|
205
282
|
|
|
206
283
|
if (methodName) {
|
|
207
284
|
console.log(` Method: ${methodName}`);
|
|
@@ -211,6 +288,10 @@ async function processInspectResult(res) {
|
|
|
211
288
|
console.log(` Include: ${sobjname}`);
|
|
212
289
|
}
|
|
213
290
|
console.log(` ${text}`);
|
|
291
|
+
if (checkCode) {
|
|
292
|
+
console.log(` [${checkClass}/${checkCode}]`);
|
|
293
|
+
console.log(` → abapgit-agent inspect --doc ${checkClass}/${checkCode}`);
|
|
294
|
+
}
|
|
214
295
|
}
|
|
215
296
|
}
|
|
216
297
|
|
|
@@ -223,6 +304,8 @@ async function processInspectResult(res) {
|
|
|
223
304
|
const text = info.MESSAGE || info.message || 'Unknown info';
|
|
224
305
|
const methodName = info.METHOD_NAME || info.method_name;
|
|
225
306
|
const sobjname = info.SOBJNAME || info.sobjname;
|
|
307
|
+
const checkClass = info.CHECK_CLASS || info.check_class || '';
|
|
308
|
+
const checkCode = info.CHECK_CODE || info.check_code || '';
|
|
226
309
|
|
|
227
310
|
if (methodName) {
|
|
228
311
|
console.log(` Method: ${methodName}`);
|
|
@@ -232,6 +315,10 @@ async function processInspectResult(res) {
|
|
|
232
315
|
console.log(` Include: ${sobjname}`);
|
|
233
316
|
}
|
|
234
317
|
console.log(` ${text}`);
|
|
318
|
+
if (checkCode) {
|
|
319
|
+
console.log(` [${checkClass}/${checkCode}]`);
|
|
320
|
+
console.log(` → abapgit-agent inspect --doc ${checkClass}/${checkCode}`);
|
|
321
|
+
}
|
|
235
322
|
}
|
|
236
323
|
}
|
|
237
324
|
} else if (success === true || success === 'X') {
|
|
@@ -254,25 +341,78 @@ module.exports = {
|
|
|
254
341
|
console.log(`
|
|
255
342
|
Usage:
|
|
256
343
|
abapgit-agent inspect --files <file1>,<file2>,... [--variant <check-variant>] [--junit-output <file>] [--json]
|
|
344
|
+
abapgit-agent inspect --doc <check_class>/<check_code>
|
|
257
345
|
|
|
258
346
|
Description:
|
|
259
347
|
Run SAP Code Inspector checks on activated ABAP objects. Requires the objects
|
|
260
348
|
to be already active in the ABAP system (run pull first).
|
|
261
349
|
|
|
350
|
+
Use --doc to fetch the SAP documentation for a specific check finding.
|
|
351
|
+
The check class and code are shown in brackets after each finding, e.g.:
|
|
352
|
+
[CL_CI_TEST_OMIT_BRACKETS/OMIT_01]
|
|
353
|
+
|
|
262
354
|
Parameters:
|
|
263
|
-
--files <file1,...> Comma-separated ABAP source files (required).
|
|
355
|
+
--files <file1,...> Comma-separated ABAP source files (required for inspection).
|
|
264
356
|
--variant <variant> Code Inspector variant (default: system default).
|
|
265
357
|
--junit-output <file> Write results as JUnit XML to this file.
|
|
266
358
|
--json Output as JSON.
|
|
359
|
+
--doc <class>/<code> Fetch documentation for a check finding.
|
|
267
360
|
|
|
268
361
|
Examples:
|
|
269
362
|
abapgit-agent inspect --files src/zcl_my_class.clas.abap
|
|
270
363
|
abapgit-agent inspect --files src/zcl_my_class.clas.abap --variant ALL_CHECKS
|
|
271
364
|
abapgit-agent inspect --files src/zcl_my_class.clas.abap --junit-output reports/inspect.xml
|
|
365
|
+
abapgit-agent inspect --doc CL_CI_TEST_OMIT_BRACKETS/OMIT_01
|
|
272
366
|
`);
|
|
273
367
|
return;
|
|
274
368
|
}
|
|
275
369
|
|
|
370
|
+
// --doc mode: fetch documentation for a check finding
|
|
371
|
+
const docArgIndex = args.indexOf('--doc');
|
|
372
|
+
if (docArgIndex !== -1) {
|
|
373
|
+
const docArg = args[docArgIndex + 1];
|
|
374
|
+
if (!docArg || !docArg.includes('/')) {
|
|
375
|
+
console.error('Error: --doc requires <check_class>/<check_code>');
|
|
376
|
+
console.error('Example: abapgit-agent inspect --doc CL_CI_TEST_OMIT_BRACKETS/OMIT_01');
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
const slashIdx = docArg.indexOf('/');
|
|
380
|
+
const checkClass = docArg.slice(0, slashIdx);
|
|
381
|
+
const checkCode = docArg.slice(slashIdx + 1);
|
|
382
|
+
|
|
383
|
+
const config = loadConfig();
|
|
384
|
+
const http = new AbapHttp(config);
|
|
385
|
+
const csrfToken = await http.fetchCsrfToken();
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const result = await http.post('/sap/bc/z_abapgit_agent/insp_doc', {
|
|
389
|
+
check_class: checkClass,
|
|
390
|
+
check_code: checkCode
|
|
391
|
+
}, { csrfToken });
|
|
392
|
+
|
|
393
|
+
const title = result.TITLE || result.title || '';
|
|
394
|
+
let text = result.TEXT || result.text || '';
|
|
395
|
+
|
|
396
|
+
// Strip ITF inline tags (<AB>, <EX>, </>, <DS:...>, etc.), HTML tags, and ITF placeholders (&...&)
|
|
397
|
+
text = text
|
|
398
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
399
|
+
.replace(/<[^>]+>/g, '')
|
|
400
|
+
.replace(/&[A-Z_]+&/g, '')
|
|
401
|
+
.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&')
|
|
402
|
+
.replace(/ /g, ' ').replace(/ /g, ' ')
|
|
403
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
404
|
+
.trim();
|
|
405
|
+
|
|
406
|
+
if (title) console.log(`\n${title}\n${'─'.repeat(Math.min(title.length, 60))}`);
|
|
407
|
+
console.log(`\n${text || 'No documentation available.'}\n`);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
const { printHttpError } = require('../utils/format-error');
|
|
410
|
+
printHttpError(error, { verbose: args.includes('--verbose') });
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
276
416
|
const jsonOutput = args.includes('--json');
|
|
277
417
|
const verbose = args.includes('--verbose');
|
|
278
418
|
const filesArgIndex = args.indexOf('--files');
|
|
@@ -285,7 +425,7 @@ Examples:
|
|
|
285
425
|
process.exit(1);
|
|
286
426
|
}
|
|
287
427
|
|
|
288
|
-
let filesSyntaxCheck = args[filesArgIndex + 1].split(',').map(f => f.trim());
|
|
428
|
+
let filesSyntaxCheck = [...new Set(args[filesArgIndex + 1].split(',').map(f => f.trim()))];
|
|
289
429
|
|
|
290
430
|
// Parse optional --variant parameter; fall back to project config
|
|
291
431
|
const variantArgIndex = args.indexOf('--variant');
|
|
@@ -338,51 +478,23 @@ Examples:
|
|
|
338
478
|
const http = new AbapHttp(config);
|
|
339
479
|
const csrfToken = await http.fetchCsrfToken();
|
|
340
480
|
|
|
341
|
-
// Send all files in one request
|
|
342
|
-
const results = await inspectAllFiles(filesSyntaxCheck, csrfToken, config, variant, http, verbose);
|
|
343
|
-
|
|
344
|
-
// Apply inspect.suppress rules — downgrade matching errors/warnings to infos
|
|
345
481
|
const suppressRules = inspectConfig.suppress || [];
|
|
346
|
-
|
|
482
|
+
let hasErrors = false;
|
|
483
|
+
|
|
484
|
+
// Stream results: apply suppress and print each object as its chunk completes
|
|
485
|
+
const onChunkResult = jsonOutput ? null : async (result) => {
|
|
486
|
+
applySuppressRules(result, suppressRules);
|
|
487
|
+
await processInspectResult(result);
|
|
488
|
+
const errorCount = result.ERROR_COUNT !== undefined ? result.ERROR_COUNT : (result.error_count || 0);
|
|
489
|
+
if (errorCount > 0) hasErrors = true;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const results = await inspectInChunks(filesSyntaxCheck, csrfToken, config, variant, http, verbose, onChunkResult);
|
|
493
|
+
|
|
494
|
+
// For JSON mode, apply suppress rules now (they were skipped during streaming)
|
|
495
|
+
if (jsonOutput) {
|
|
347
496
|
for (const result of results) {
|
|
348
|
-
|
|
349
|
-
for (const rule of suppressRules) {
|
|
350
|
-
const objPattern = new RegExp('^' + (rule.object || '*').toUpperCase().replace(/\*/g, '.*') + '$');
|
|
351
|
-
if (!objPattern.test(objName)) continue;
|
|
352
|
-
const msgPattern = new RegExp((rule.message || '*').replace(/\*/g, '.*'), 'i');
|
|
353
|
-
|
|
354
|
-
// Downgrade matching errors → infos
|
|
355
|
-
const errors = result.ERRORS || result.errors || [];
|
|
356
|
-
const kept = [];
|
|
357
|
-
for (const err of errors) {
|
|
358
|
-
const text = err.TEXT || err.text || '';
|
|
359
|
-
if (msgPattern.test(text)) {
|
|
360
|
-
const infos = result.INFOS || result.infos || [];
|
|
361
|
-
infos.push({ ...err, MESSAGE: `[suppressed] ${text}`, message: `[suppressed] ${text}` });
|
|
362
|
-
if (result.INFOS !== undefined) result.INFOS = infos; else result.infos = infos;
|
|
363
|
-
const ec = result.ERROR_COUNT !== undefined ? 'ERROR_COUNT' : 'error_count';
|
|
364
|
-
result[ec] = Math.max(0, (result[ec] || 0) - 1);
|
|
365
|
-
} else {
|
|
366
|
-
kept.push(err);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
if (result.ERRORS !== undefined) result.ERRORS = kept; else result.errors = kept;
|
|
370
|
-
|
|
371
|
-
// Downgrade matching warnings → infos
|
|
372
|
-
const warnings = result.WARNINGS || result.warnings || [];
|
|
373
|
-
const keptW = [];
|
|
374
|
-
for (const warn of warnings) {
|
|
375
|
-
const text = warn.MESSAGE || warn.message || '';
|
|
376
|
-
if (msgPattern.test(text)) {
|
|
377
|
-
const infos = result.INFOS || result.infos || [];
|
|
378
|
-
infos.push({ ...warn, MESSAGE: `[suppressed] ${text}`, message: `[suppressed] ${text}` });
|
|
379
|
-
if (result.INFOS !== undefined) result.INFOS = infos; else result.infos = infos;
|
|
380
|
-
} else {
|
|
381
|
-
keptW.push(warn);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
if (result.WARNINGS !== undefined) result.WARNINGS = keptW; else result.warnings = keptW;
|
|
385
|
-
}
|
|
497
|
+
applySuppressRules(result, suppressRules);
|
|
386
498
|
}
|
|
387
499
|
}
|
|
388
500
|
|
|
@@ -408,14 +520,6 @@ Examples:
|
|
|
408
520
|
return;
|
|
409
521
|
}
|
|
410
522
|
|
|
411
|
-
// Process results
|
|
412
|
-
let hasErrors = false;
|
|
413
|
-
for (const result of results) {
|
|
414
|
-
await processInspectResult(result);
|
|
415
|
-
const errorCount = result.ERROR_COUNT !== undefined ? result.ERROR_COUNT : (result.error_count || 0);
|
|
416
|
-
if (errorCount > 0) hasErrors = true;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
523
|
if (hasErrors) {
|
|
420
524
|
process.exit(1);
|
|
421
525
|
}
|