abapgit-agent 1.15.2 → 1.16.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/.github/copilot-instructions.slim.md +35 -4
- package/abap/CLAUDE.md +62 -10
- package/abap/guidelines/abapgit-fugr.md +19 -3
- package/abap/guidelines/abapgit-xml-only.md +68 -12
- package/abap/guidelines/abapgit.md +2 -1
- package/abap/guidelines/abaplint.md +35 -0
- package/abap/guidelines/common-errors.md +78 -1
- package/abap/guidelines/index.md +1 -0
- package/abap/guidelines/naming-limits.md +160 -0
- package/abap/guidelines/object-creation.md +1 -1
- package/abap/guidelines/objects.md +5 -2
- package/abap/guidelines/string-template.md +84 -0
- package/package.json +2 -1
- package/src/commands/drop.js +171 -0
- package/src/commands/index.js +2 -1
- package/src/commands/syntax.js +121 -17
- package/abap/.github/copilot-instructions.md +0 -264
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: Naming Length Limits
|
|
4
|
+
nav_order: 6
|
|
5
|
+
parent: ABAP Coding Guidelines
|
|
6
|
+
grand_parent: ABAP Development
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# ABAP Naming Length Limits
|
|
10
|
+
|
|
11
|
+
**Searchable keywords**: name length, character limit, 30 characters, 16 characters, 40 characters, too long, truncate, field name, method name, class name, table name, CDS name
|
|
12
|
+
|
|
13
|
+
> **Rule of thumb**: Most ABAP object names → 30 chars. Table/structure **field names** → 16 chars. CDS/DDLS names → 40 chars. Method names (including test methods) → 30 chars.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Quick Reference
|
|
18
|
+
|
|
19
|
+
| Object / Element | Max Length |
|
|
20
|
+
|---|---|
|
|
21
|
+
| Class name (CLAS) | **30** |
|
|
22
|
+
| Interface name (INTF) | **30** |
|
|
23
|
+
| Program name (PROG) | **30** |
|
|
24
|
+
| Function Group name (FUGR) | **26** |
|
|
25
|
+
| Function Module name | **30** |
|
|
26
|
+
| Table name (TABL) | **30** |
|
|
27
|
+
| **Table field name** | **16** |
|
|
28
|
+
| Structure name (STRU) | **30** |
|
|
29
|
+
| **Structure field name** | **16** |
|
|
30
|
+
| Data Element name (DTEL) | **30** |
|
|
31
|
+
| Domain name (DOMA) | **30** |
|
|
32
|
+
| Table Type name (TTYP) | **30** |
|
|
33
|
+
| Package name | **30** |
|
|
34
|
+
| **CDS View Entity name (DDLS)** | **40** |
|
|
35
|
+
| CDS Access Control name (DCLS) | **40** |
|
|
36
|
+
| CDS field alias | **30** |
|
|
37
|
+
| Message Class name (MSAG) | **20** |
|
|
38
|
+
| Class method name | **30** |
|
|
39
|
+
| **Test method name** | **30** |
|
|
40
|
+
| Interface method name | **30** |
|
|
41
|
+
| Class attribute name | **30** |
|
|
42
|
+
| Local variable name | **30** |
|
|
43
|
+
| Local type/class name | **30** |
|
|
44
|
+
| Test class name (local) | **30** |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Critical Differences — Don't Confuse These
|
|
49
|
+
|
|
50
|
+
### Table/Structure Field Names: 16 Characters MAX
|
|
51
|
+
|
|
52
|
+
This is the **most common mistake**. Field names in TABL and STRU are limited to **16 characters**, not 30.
|
|
53
|
+
|
|
54
|
+
```xml
|
|
55
|
+
<!-- WRONG — 17 characters -->
|
|
56
|
+
<FIELDNAME>LAST_MODIFIED_AT</FIELDNAME>
|
|
57
|
+
|
|
58
|
+
<!-- CORRECT — 16 characters or fewer -->
|
|
59
|
+
<FIELDNAME>LAST_MODIFIED</FIELDNAME>
|
|
60
|
+
<FIELDNAME>SYS_CHANGED_AT</FIELDNAME> <!-- 14 chars ✓ -->
|
|
61
|
+
<FIELDNAME>LAST_PULLED_AT</FIELDNAME> <!-- 14 chars ✓ -->
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
When naming table fields, keep names short and descriptive:
|
|
65
|
+
- `CARRID` not `CARRIER_ID_FIELD`
|
|
66
|
+
- `CONNID` not `CONNECTION_IDENTIFIER`
|
|
67
|
+
- `STATUS` not `CURRENT_STATUS_FLAG`
|
|
68
|
+
- `CREATED_AT` not `CREATION_TIMESTAMP`
|
|
69
|
+
|
|
70
|
+
### CDS View Names: 40 Characters MAX
|
|
71
|
+
|
|
72
|
+
CDS View Entity (DDLS) names allow up to **40 characters** — more room than regular ABAP objects.
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
ZC_MY_FLIGHT_BOOKING_REVENUE_SUMMARY ← 40 chars (at limit)
|
|
76
|
+
ZC_FLIGHT_REVENUE ← 17 chars (fine)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
However, CDS **field aliases** inside the view are still limited to **30 characters** (ABAP identifier rules).
|
|
80
|
+
|
|
81
|
+
### Function Group Names: 26 Characters MAX
|
|
82
|
+
|
|
83
|
+
Function groups (`FUGR`) have a **26-character limit** because ABAP appends a 4-character suffix internally (e.g. `SAPLZMY_FG` prefix + module name). The safe usable name length is 26 characters.
|
|
84
|
+
|
|
85
|
+
### Test Method Names: 30 Characters MAX — Causes Syntax Error
|
|
86
|
+
|
|
87
|
+
Test methods (`FOR TESTING`) hit the 30-char limit frequently because the `test_` prefix takes 5 chars before the meaningful content starts.
|
|
88
|
+
|
|
89
|
+
```abap
|
|
90
|
+
" WRONG — 34 characters → syntax error at activation
|
|
91
|
+
METHODS test_execute_with_minimal_params FOR TESTING.
|
|
92
|
+
|
|
93
|
+
" CORRECT — abbreviate to stay within 30 chars
|
|
94
|
+
METHODS test_exec_minimal FOR TESTING. " 18 chars ✓
|
|
95
|
+
METHODS test_exec_with_files FOR TESTING. " 24 chars ✓
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Counting test method length**: include the full method name — `test_exec_minimal` is 18 characters.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Counting Characters Before You Name Things
|
|
103
|
+
|
|
104
|
+
Use this mental check before naming any ABAP element:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
# Object name: type prefix + your name ≤ limit
|
|
108
|
+
ZCL_ (4 chars) + name ≤ 30 → name ≤ 26 chars
|
|
109
|
+
ZIF_ (4 chars) + name ≤ 30 → name ≤ 26 chars
|
|
110
|
+
ZC_ (3 chars) + name ≤ 40 → name ≤ 37 chars (CDS)
|
|
111
|
+
Z (1 char) + name ≤ 30 → name ≤ 29 chars (table/program)
|
|
112
|
+
|
|
113
|
+
# Project-specific sub-namespace eats more of the budget — plan ahead
|
|
114
|
+
# Example: project uses ZFICO_ prefix for all objects
|
|
115
|
+
ZCL_FICO_ (9 chars) + name ≤ 30 → name ≤ 21 chars
|
|
116
|
+
ZCL_FICO_PAYMENT_PROPOSAL = 26 chars ✓
|
|
117
|
+
ZCL_FICO_PAYMENT_PROPOSAL_V = 27 chars ✓ (getting tight)
|
|
118
|
+
|
|
119
|
+
# Field name in TABL/STRU: no prefix, just ≤ 16 total
|
|
120
|
+
PAYMENT_METHOD = 14 chars ✓
|
|
121
|
+
PAYMENT_METHOD_CD = 17 chars ✗ → shorten to PAYMENT_METH_CD
|
|
122
|
+
|
|
123
|
+
# Method name: no prefix, just ≤ 30 total
|
|
124
|
+
test_exec_with_files → 24 chars ✓
|
|
125
|
+
test_execute_with_minimal_params → 34 chars ✗
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Common Length Violations and Fixes
|
|
131
|
+
|
|
132
|
+
| Too Long (violates limit) | Fixed Version | Limit |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `ZCL_COMMAND_PULL_WITH_RETRY` (30+ chars) | `ZCL_COMMAND_PULL_RETRY` | 30 |
|
|
135
|
+
| `LAST_SUCCESSFULLY_PULLED_AT` (table field, 28 chars) | `LAST_PULLED_AT` | 16 |
|
|
136
|
+
| `test_execute_command_with_files` (test method, 32 chars) | `test_exec_with_files` | 30 |
|
|
137
|
+
| `ZC_MY_VERY_LONG_CDS_VIEW_NAME_EXCEEDS_40_CHARS` (47 chars) | `ZC_MY_LONG_CDS_VIEW_NAME_TRIMMED` | 40 |
|
|
138
|
+
| `ZBIZ_OBJECT_CREATION_SERVICE_MESSAGE_CLASS` (MSAG, 43 chars) | `ZBIZ_CREATE_MSGS` | 20 |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## SAP Technical Basis for These Limits
|
|
143
|
+
|
|
144
|
+
These limits come from the ABAP Dictionary (DDIC) and ABAP kernel:
|
|
145
|
+
|
|
146
|
+
| Limit Source | Explanation |
|
|
147
|
+
|---|---|
|
|
148
|
+
| 30 chars (most objects) | ABAP uses `RSYN` program name space; objects stored in `TADIR` with `SOBJ_NAME CHAR(40)` but compiler enforces 30 for classes/interfaces/programs |
|
|
149
|
+
| 16 chars (DDIC fields) | Stored in `DD03L.FIELDNAME CHAR(16)` — this is a hard database column width |
|
|
150
|
+
| 40 chars (CDS names) | CDS objects stored in `DD02L.TABNAME CHAR(40)` — intentionally larger for CDS |
|
|
151
|
+
| 20 chars (MSAG) | Message class name stored in `T100A.ARBGB CHAR(20)` |
|
|
152
|
+
| 26 chars (FUGR) | Function group internally prefixed with `SAPL` (4 chars) for the main include |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## See Also
|
|
157
|
+
|
|
158
|
+
- **Naming Conventions** (objects.md) — prefixes per object type
|
|
159
|
+
- **Object Creation** (object-creation.md) — which files to create
|
|
160
|
+
- **Testing** (testing.md) — test method naming (30-char limit detail)
|
|
@@ -24,7 +24,7 @@ Replace `<name>` with the actual object name from this project's naming conventi
|
|
|
24
24
|
| CDS Access Control (DCLS) | `<name>.dcls.asdcls` | `<name>.dcls.xml` | `ref --topic abapgit` |
|
|
25
25
|
| Function Group (FUGR) | `<name>.fugr.abap` + includes | `<name>.fugr.xml` | `ref --topic abapgit` |
|
|
26
26
|
| Table (TABL) | *(none — XML-only)* | `<name>.tabl.xml` | `ref --topic abapgit-xml-only` |
|
|
27
|
-
| Structure (STRU) | *(none — XML-only)* | `<name>.stru.xml` | `ref --topic abapgit-xml-only` |
|
|
27
|
+
| Structure (STRU) | *(none — XML-only)* | `<name>.tabl.xml` ⚠️ NOT `.stru.xml` | `ref --topic abapgit-xml-only` |
|
|
28
28
|
| Data Element (DTEL) | *(none — XML-only)* | `<name>.dtel.xml` | `ref --topic abapgit-xml-only` |
|
|
29
29
|
| Table Type (TTYP) | *(none — XML-only)* | `<name>.ttyp.xml` | `ref --topic abapgit-xml-only` |
|
|
30
30
|
| Domain (DOMA) | *(none — XML-only)* | `<name>.doma.xml` | `ref --topic abapgit-xml-only` |
|
|
@@ -15,12 +15,13 @@ grand_parent: ABAP Development
|
|
|
15
15
|
| | SAP Namespace | Customer Namespace |
|
|
16
16
|
|---|---|---|
|
|
17
17
|
| **Object prefix** | `CL_*`, `IF_*`, `/NAME/CL_*`, `/NAME/IF_*`, etc. | `Z*`, `Y*` |
|
|
18
|
-
| **Package prefix** | SAP-delivered (e.g. `SFIN`, `CA_*`, `/NAME/*`) | `Z*`, `Y*`, `$*` |
|
|
18
|
+
| **Package prefix** | SAP-delivered (e.g. `SFIN`, `CA_*`, `/NAME/*`) | `Z*`, `Y*`, `$*` (local/non-transportable) |
|
|
19
19
|
| **Ownership** | Delivered and maintained by SAP | Owned by the customer |
|
|
20
20
|
| **In git repo** | Objects from an SAP-delivered package | Custom development objects |
|
|
21
21
|
|
|
22
22
|
> **Rule**: Never add customer-created objects (including PoC/probe classes) into SAP namespace
|
|
23
23
|
> packages. PoC objects always use `Z*`/`Y*` prefix and always go in a `Z*`, `Y*`, or `$*` package
|
|
24
|
+
> (use `$*` only for throwaway probes — non-transportable)
|
|
24
25
|
> — even on a project where production objects use SAP namespace.
|
|
25
26
|
|
|
26
27
|
## How to Determine This Project's Naming Convention
|
|
@@ -43,7 +44,8 @@ Applied when no `objects.local.md` exists:
|
|
|
43
44
|
| Class | `ZCL_` | `ZCL_MY_CLASS` |
|
|
44
45
|
| Interface | `ZIF_` | `ZIF_MY_INTERFACE` |
|
|
45
46
|
| Program | `Z` | `ZMY_PROGRAM` |
|
|
46
|
-
| Package |
|
|
47
|
+
| Package | `Z` or `Y` | `ZMYPROJECT` |
|
|
48
|
+
> **Note**: `$` packages (e.g. `$TMP`) are local/non-transportable — use only for throwaway probe objects, never for real development.
|
|
47
49
|
| Table | `Z` | `ZMY_TABLE` |
|
|
48
50
|
| CDS View Entity | `ZC_` | `ZC_MY_VIEW` |
|
|
49
51
|
| Data Element | `Z` | `ZMY_ELEMENT` |
|
|
@@ -83,3 +85,4 @@ Default package: /MYNAMESPACE/MAIN
|
|
|
83
85
|
|
|
84
86
|
→ For file structure (what files to create): `abapgit-agent ref --topic object-creation`
|
|
85
87
|
→ For XML templates: `abapgit-agent ref --topic abapgit`
|
|
88
|
+
→ For name **length limits** (30/16/40 char rules): `abapgit-agent ref --topic naming-limits`
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: default
|
|
3
|
+
title: String Templates
|
|
4
|
+
nav_order: 25
|
|
5
|
+
parent: ABAP Coding Guidelines
|
|
6
|
+
grand_parent: ABAP Development
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# String Templates
|
|
10
|
+
|
|
11
|
+
**Searchable keywords**: string template, pipe, `|...|`, literal brace, escape, expression limiter, JSON, `\{`, `\}`
|
|
12
|
+
|
|
13
|
+
## Syntax
|
|
14
|
+
|
|
15
|
+
String templates are delimited by `|...|`. Embedded expressions use `{ expr }`:
|
|
16
|
+
|
|
17
|
+
```abap
|
|
18
|
+
DATA(s) = |Hello { lv_name }!|.
|
|
19
|
+
DATA(s) = |User: { cl_abap_context_info=>get_user_technical_name( ) }|.
|
|
20
|
+
DATA(s) = |Length: { strlen( lv_text ) }|.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Escaping Special Characters
|
|
24
|
+
|
|
25
|
+
Inside `|...|`, four characters have special meaning and must be escaped with `\`:
|
|
26
|
+
|
|
27
|
+
| Character | Escape as | Produces |
|
|
28
|
+
|-----------|-----------|---------|
|
|
29
|
+
| `{` | `\{` | literal `{` |
|
|
30
|
+
| `}` | `\}` | literal `}` |
|
|
31
|
+
| `\` | `\\` | literal `\` |
|
|
32
|
+
| `\|` | `\|` | literal `\|` |
|
|
33
|
+
|
|
34
|
+
```abap
|
|
35
|
+
" Produces: \ | { }
|
|
36
|
+
DATA(s) = |\\ \| \{ \}|.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## JSON Payloads — The Most Common Mistake
|
|
40
|
+
|
|
41
|
+
JSON objects start with `{` — which ABAP treats as an expression delimiter.
|
|
42
|
+
**Always escape outer JSON braces as `\{` and `\}`.**
|
|
43
|
+
|
|
44
|
+
```abap
|
|
45
|
+
" WRONG — "Expression limiter '{' in string template not followed by space"
|
|
46
|
+
rv_result = |{"success":"X","name":"{ lv_name }"}|.
|
|
47
|
+
" ^ unescaped { triggers parse error
|
|
48
|
+
|
|
49
|
+
" CORRECT — outer JSON braces escaped, expression { } left as-is
|
|
50
|
+
rv_result = |\{"success":"X","name":"{ lv_name }"\}|.
|
|
51
|
+
|
|
52
|
+
" CORRECT — error with method call
|
|
53
|
+
rv_result = |\{"success":"","error":"{ lx_error->get_text( ) }"\}|.
|
|
54
|
+
|
|
55
|
+
" CORRECT — multiple embedded fields
|
|
56
|
+
rv_result = |\{"success":"X","object":"{ lv_obj_name }","type":"{ lv_obj_type }"\}|.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Control Characters
|
|
60
|
+
|
|
61
|
+
```abap
|
|
62
|
+
DATA(s) = |line1\nline2|. " newline
|
|
63
|
+
DATA(s) = |col1\tcol2|. " tab
|
|
64
|
+
DATA(s) = |line1\r\nline2|. " CR+LF
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Chaining with `&&`
|
|
68
|
+
|
|
69
|
+
```abap
|
|
70
|
+
DATA(s) = |{ lv_a }| && ` separator ` && |{ lv_b }|.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Performance Note
|
|
74
|
+
|
|
75
|
+
A string template containing only literal text (no `{ }` expressions) is evaluated at
|
|
76
|
+
runtime. Prefer backquote literals for pure text:
|
|
77
|
+
|
|
78
|
+
```abap
|
|
79
|
+
" Prefer this for literal-only strings
|
|
80
|
+
DATA(s) = `Hello World`.
|
|
81
|
+
|
|
82
|
+
" Not this (runtime overhead with no benefit)
|
|
83
|
+
DATA(s) = |Hello World|.
|
|
84
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abapgit-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
|
|
5
5
|
"files": [
|
|
6
6
|
"bin/",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"test:cmd:preview": "node tests/run-all.js --cmd --command=preview",
|
|
37
37
|
"test:cmd:tree": "node tests/run-all.js --cmd --command=tree",
|
|
38
38
|
"test:cmd:dump": "node tests/run-all.js --cmd --command=dump",
|
|
39
|
+
"test:drop": "node tests/run-all.js --drop",
|
|
39
40
|
"test:cmd:debug": "node tests/run-all.js --cmd --command=debug",
|
|
40
41
|
"test:debug": "node tests/run-all.js --debug",
|
|
41
42
|
"test:debug:scenarios": "bash tests/integration/debug-scenarios.sh",
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop command - Physically delete a single ABAP object from the ABAP system
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { printHttpError } = require('../utils/format-error');
|
|
6
|
+
|
|
7
|
+
// Map file extension (second-to-last part) to ABAP object type label
|
|
8
|
+
const EXT_TO_TYPE = {
|
|
9
|
+
clas: 'CLAS', intf: 'INTF', prog: 'PROG', fugr: 'FUGR',
|
|
10
|
+
tabl: 'TABL', dtel: 'DTEL', ttyp: 'TTYP', doma: 'DOMA',
|
|
11
|
+
ddls: 'DDLS', dcls: 'DCLS', msag: 'MSAG', stru: 'STRU'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Derive a display label for the object from the file path.
|
|
16
|
+
* e.g. "abap/zcl_foo.clas.abap" → { name: "ZCL_FOO", type: "CLAS" }
|
|
17
|
+
*/
|
|
18
|
+
function objectFromFile(filePath) {
|
|
19
|
+
const base = filePath.split('/').pop();
|
|
20
|
+
const parts = base.split('.');
|
|
21
|
+
if (parts.length < 3) return null;
|
|
22
|
+
const name = parts[0].toUpperCase();
|
|
23
|
+
const typeExt = parts[1].toLowerCase();
|
|
24
|
+
const type = EXT_TO_TYPE[typeExt] || typeExt.toUpperCase();
|
|
25
|
+
return { name, type };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
name: 'drop',
|
|
30
|
+
description: 'Physically delete a single ABAP object from the ABAP system',
|
|
31
|
+
requiresAbapConfig: true,
|
|
32
|
+
requiresVersionCheck: true,
|
|
33
|
+
|
|
34
|
+
async execute(args, context) {
|
|
35
|
+
const { loadConfig, AbapHttp, gitUtils, getTransport, getConflictSettings } = context;
|
|
36
|
+
|
|
37
|
+
// Show help if requested
|
|
38
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
39
|
+
console.log(`
|
|
40
|
+
Usage:
|
|
41
|
+
abapgit-agent drop --files <file>
|
|
42
|
+
abapgit-agent drop --files <file> --transport <TRANSPORT>
|
|
43
|
+
abapgit-agent drop --files <file> --pull
|
|
44
|
+
|
|
45
|
+
Description:
|
|
46
|
+
Physically deletes a single ABAP object from the ABAP system using abapGit's
|
|
47
|
+
own object serializer. The object type and name are derived from the file path.
|
|
48
|
+
|
|
49
|
+
Use --pull to immediately re-activate the object from git after deletion
|
|
50
|
+
(useful to force a clean re-installation).
|
|
51
|
+
|
|
52
|
+
Parameters:
|
|
53
|
+
--files <file> Path to the ABAP source or XML file (required).
|
|
54
|
+
Accepted extensions: .abap, .asddls, .tabl.xml, etc.
|
|
55
|
+
The file must exist on disk.
|
|
56
|
+
--transport <TRANSPORT> Transport request (e.g. DEVK900001). Optional.
|
|
57
|
+
--pull Re-activate the object via pull after deletion.
|
|
58
|
+
--conflict-mode <mode> Conflict mode for --pull: abort (default) or ignore.
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
abapgit-agent drop --files abap/zcl_foo.clas.abap
|
|
62
|
+
abapgit-agent drop --files abap/zcl_foo.clas.abap --pull
|
|
63
|
+
abapgit-agent drop --files abap/zmy_table.tabl.xml --transport DEVK900001
|
|
64
|
+
`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const filesArgIndex = args.indexOf('--files');
|
|
69
|
+
const transportArgIndex = args.indexOf('--transport');
|
|
70
|
+
const conflictModeArgIndex = args.indexOf('--conflict-mode');
|
|
71
|
+
const doPull = args.includes('--pull');
|
|
72
|
+
|
|
73
|
+
if (filesArgIndex === -1 || filesArgIndex + 1 >= args.length) {
|
|
74
|
+
console.error('❌ Error: --files is required');
|
|
75
|
+
console.error(' Usage: abapgit-agent drop --files <file>');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const filePath = args[filesArgIndex + 1].trim();
|
|
80
|
+
|
|
81
|
+
// Validate file exists on disk
|
|
82
|
+
const fs = require('fs');
|
|
83
|
+
if (!fs.existsSync(filePath)) {
|
|
84
|
+
console.error(`❌ Error: file not found: ${filePath}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Validate file extension (same rules as pull --files)
|
|
89
|
+
const base = filePath.split('/').pop();
|
|
90
|
+
const parts = base.split('.');
|
|
91
|
+
const lastExt = parts[parts.length - 1].toLowerCase();
|
|
92
|
+
const ABAP_SOURCE_EXTS = new Set(['abap', 'asddls']);
|
|
93
|
+
const isXmlOnlyObject = parts.length === 3 && parts[0].length > 0 && lastExt === 'xml';
|
|
94
|
+
if (!ABAP_SOURCE_EXTS.has(lastExt) && !isXmlOnlyObject) {
|
|
95
|
+
console.error('❌ Error: --files must be an ABAP source file (.abap, .asddls) or an XML-only object file (name.type.xml).');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const obj = objectFromFile(filePath);
|
|
100
|
+
|
|
101
|
+
if (obj && obj.type === 'DTEL') {
|
|
102
|
+
console.error(`❌ drop does not support DTEL objects.`);
|
|
103
|
+
console.error(` Data elements cannot be re-activated after deletion due to SAP CBDA`);
|
|
104
|
+
console.error(` activation engine limitations. Edit the XML file and run pull instead.`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const transportRequest = transportArgIndex !== -1 ? args[transportArgIndex + 1] : (getTransport ? getTransport() : null);
|
|
109
|
+
const conflictMode = conflictModeArgIndex !== -1 ? args[conflictModeArgIndex + 1] : (getConflictSettings ? getConflictSettings().mode : 'abort');
|
|
110
|
+
|
|
111
|
+
console.log(`\n🗑️ Dropping ${obj ? obj.type + ' ' + obj.name : filePath} from ABAP system...`);
|
|
112
|
+
if (transportRequest) {
|
|
113
|
+
console.log(` Transport: ${transportRequest}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const config = loadConfig();
|
|
117
|
+
const http = new AbapHttp(config);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const csrfToken = await http.fetchCsrfToken();
|
|
121
|
+
|
|
122
|
+
const data = { file: filePath };
|
|
123
|
+
if (transportRequest) {
|
|
124
|
+
data.transport_request = transportRequest;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = await http.post('/sap/bc/z_abapgit_agent/drop', data, { csrfToken });
|
|
128
|
+
|
|
129
|
+
const success = result.SUCCESS || result.success;
|
|
130
|
+
const objectName = result.OBJECT || result.object;
|
|
131
|
+
const objectType = result.TYPE || result.type;
|
|
132
|
+
const message = result.MESSAGE || result.message;
|
|
133
|
+
const error = result.ERROR || result.error;
|
|
134
|
+
|
|
135
|
+
if (success === 'X' || success === true) {
|
|
136
|
+
console.log(`✅ Object deleted successfully.`);
|
|
137
|
+
if (objectName && objectType) {
|
|
138
|
+
console.log(` Object: ${objectName} (${objectType})`);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
console.error(`❌ Failed to delete object`);
|
|
142
|
+
console.error(` Error: ${error || message || 'Unknown error'}`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
printHttpError(error, {});
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// --pull: re-activate the object from git
|
|
151
|
+
if (doPull) {
|
|
152
|
+
console.log(`\n↩️ Re-pulling from git...`);
|
|
153
|
+
const pullCommand = require('./pull');
|
|
154
|
+
const gitUrl = gitUtils.getRemoteUrl();
|
|
155
|
+
const branch = gitUtils.getBranch();
|
|
156
|
+
if (!gitUrl) {
|
|
157
|
+
console.error('❌ Cannot re-pull: no git remote configured.');
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
await pullCommand.pull(
|
|
162
|
+
gitUrl, branch, [filePath], transportRequest || null,
|
|
163
|
+
loadConfig, AbapHttp, false, undefined, conflictMode, false, false
|
|
164
|
+
);
|
|
165
|
+
} catch (pullError) {
|
|
166
|
+
// pull() already printed the error
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
package/src/commands/index.js
CHANGED
package/src/commands/syntax.js
CHANGED
|
@@ -48,6 +48,7 @@ module.exports = {
|
|
|
48
48
|
// Build objects array from files
|
|
49
49
|
// Group class files together (main + locals)
|
|
50
50
|
const classFilesMap = new Map(); // className -> { main, locals_def, locals_imp }
|
|
51
|
+
const fugrGroupMap = new Map(); // groupName -> { dir, fmFiles: Map<fmName, source> }
|
|
51
52
|
const objects = [];
|
|
52
53
|
|
|
53
54
|
for (const file of syntaxFiles) {
|
|
@@ -83,6 +84,35 @@ module.exports = {
|
|
|
83
84
|
objType = 'DDLS';
|
|
84
85
|
objName = baseName.split('.')[0].toUpperCase();
|
|
85
86
|
fileKind = 'main';
|
|
87
|
+
} else if (baseName.includes('.fugr.')) {
|
|
88
|
+
objType = 'FUGR';
|
|
89
|
+
const parts = baseName.split('.'); // e.g. ['zmy_fugr', 'fugr', 'zmy_my_function', 'abap']
|
|
90
|
+
objName = parts[0].toUpperCase(); // group name e.g. 'ZMY_FUGR'
|
|
91
|
+
const includeFile = parts[2] || ''; // e.g. 'zmy_my_function', 'lzmy_fugrtop', 'saplzmy_fugr'
|
|
92
|
+
const groupLower = parts[0].toLowerCase();
|
|
93
|
+
const isTopInclude = new RegExp(`^l${groupLower}top$`, 'i').test(includeFile);
|
|
94
|
+
const isUInclude = new RegExp(`^l${groupLower}u\\d+$`, 'i').test(includeFile);
|
|
95
|
+
const isSapl = includeFile.toLowerCase().startsWith('sapl');
|
|
96
|
+
const isFm = !isTopInclude && !isUInclude && !isSapl && includeFile !== '';
|
|
97
|
+
|
|
98
|
+
// Read source and store in fugrGroupMap
|
|
99
|
+
const filePath = pathModule.resolve(file);
|
|
100
|
+
if (!fs.existsSync(filePath)) {
|
|
101
|
+
console.error(` Error: File not found: ${file}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
105
|
+
const dir = pathModule.dirname(filePath);
|
|
106
|
+
|
|
107
|
+
if (!fugrGroupMap.has(objName)) {
|
|
108
|
+
fugrGroupMap.set(objName, { dir, fmFiles: new Map() });
|
|
109
|
+
}
|
|
110
|
+
if (isFm) {
|
|
111
|
+
const fmName = includeFile.toUpperCase();
|
|
112
|
+
fugrGroupMap.get(objName).fmFiles.set(fmName, source);
|
|
113
|
+
}
|
|
114
|
+
// Skip adding to objects here — handled after auto-detection below
|
|
115
|
+
continue;
|
|
86
116
|
}
|
|
87
117
|
|
|
88
118
|
// Read source from file
|
|
@@ -236,12 +266,74 @@ module.exports = {
|
|
|
236
266
|
}
|
|
237
267
|
}
|
|
238
268
|
|
|
269
|
+
// Helper: read FIXPT from SAPL XML for a function group
|
|
270
|
+
function readFugrFixpt(dir, groupName) {
|
|
271
|
+
const xmlFile = pathModule.join(dir, `${groupName.toLowerCase()}.fugr.sapl${groupName.toLowerCase()}.xml`);
|
|
272
|
+
if (fs.existsSync(xmlFile)) {
|
|
273
|
+
const xmlContent = fs.readFileSync(xmlFile, 'utf8');
|
|
274
|
+
const fixptMatch = xmlContent.match(/<FIXPT>([^<]+)<\/FIXPT>/);
|
|
275
|
+
if (fixptMatch && fixptMatch[1] === 'X') return 'X';
|
|
276
|
+
}
|
|
277
|
+
return '';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Helper: classify a FUGR include filename — returns fm name (uppercase) or null if not an FM file
|
|
281
|
+
function getFugrFmName(includeFile, groupLower) {
|
|
282
|
+
if (!includeFile) return null;
|
|
283
|
+
const isTopInclude = new RegExp(`^l${groupLower}top$`, 'i').test(includeFile);
|
|
284
|
+
const isUInclude = new RegExp(`^l${groupLower}u\\d+$`, 'i').test(includeFile);
|
|
285
|
+
const isSapl = includeFile.toLowerCase().startsWith('sapl');
|
|
286
|
+
if (isTopInclude || isUInclude || isSapl) return null;
|
|
287
|
+
return includeFile.toUpperCase();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Auto-detect all FM files for each function group and build objects
|
|
291
|
+
for (const [groupName, groupData] of fugrGroupMap) {
|
|
292
|
+
const { dir } = groupData;
|
|
293
|
+
const groupLower = groupName.toLowerCase();
|
|
294
|
+
const prefix = `${groupLower}.fugr.`;
|
|
295
|
+
|
|
296
|
+
// Scan directory for all FUGR files belonging to this group
|
|
297
|
+
let allFiles;
|
|
298
|
+
try {
|
|
299
|
+
allFiles = fs.readdirSync(dir);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
allFiles = [];
|
|
302
|
+
}
|
|
303
|
+
for (const f of allFiles) {
|
|
304
|
+
if (!f.toLowerCase().startsWith(prefix) || !f.toLowerCase().endsWith('.abap')) continue;
|
|
305
|
+
const parts = f.split('.');
|
|
306
|
+
const includeFile = parts[2] || '';
|
|
307
|
+
const fmName = getFugrFmName(includeFile, groupLower);
|
|
308
|
+
if (fmName && !groupData.fmFiles.has(fmName)) {
|
|
309
|
+
groupData.fmFiles.set(fmName, fs.readFileSync(pathModule.join(dir, f), 'utf8'));
|
|
310
|
+
if (!jsonOutput) console.log(` Auto-detected: ${f}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (groupData.fmFiles.size === 0) {
|
|
315
|
+
if (!jsonOutput) console.error(` Warning: No FM source files found for FUGR ${groupName}`);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const fixpt = readFugrFixpt(dir, groupName);
|
|
320
|
+
|
|
321
|
+
// Add one object per FM
|
|
322
|
+
for (const [fmName, source] of groupData.fmFiles) {
|
|
323
|
+
objects.push({
|
|
324
|
+
type: 'FUGR',
|
|
325
|
+
name: groupName,
|
|
326
|
+
source: source,
|
|
327
|
+
fugr_include_name: fmName,
|
|
328
|
+
fixpt: fixpt
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
239
333
|
if (objects.length === 0) {
|
|
240
334
|
console.error(' No valid files to check');
|
|
241
335
|
process.exit(1);
|
|
242
336
|
}
|
|
243
|
-
|
|
244
|
-
// Send request
|
|
245
337
|
const data = {
|
|
246
338
|
objects: objects,
|
|
247
339
|
uccheck: cloudMode ? '5' : 'X'
|
|
@@ -258,7 +350,8 @@ module.exports = {
|
|
|
258
350
|
console.log(JSON.stringify(result, null, 2));
|
|
259
351
|
} else {
|
|
260
352
|
// Display results for each object
|
|
261
|
-
for (
|
|
353
|
+
for (let i = 0; i < results.length; i++) {
|
|
354
|
+
const res = results[i];
|
|
262
355
|
const objSuccess = res.SUCCESS !== undefined ? res.SUCCESS : res.success;
|
|
263
356
|
const objType = res.OBJECT_TYPE || res.object_type || 'UNKNOWN';
|
|
264
357
|
const objName = res.OBJECT_NAME || res.object_name || 'UNKNOWN';
|
|
@@ -267,13 +360,18 @@ module.exports = {
|
|
|
267
360
|
const warnings = res.WARNINGS || res.warnings || [];
|
|
268
361
|
const objMessage = res.MESSAGE || res.message || '';
|
|
269
362
|
|
|
363
|
+
// For FUGR: show which FM was checked alongside the group name
|
|
364
|
+
const sentObj = objects[i] || {};
|
|
365
|
+
const fugrFmLabel = (objType === 'FUGR' && sentObj.fugr_include_name)
|
|
366
|
+
? ` (${sentObj.fugr_include_name})` : '';
|
|
367
|
+
|
|
270
368
|
if (objSuccess) {
|
|
271
|
-
console.log(`✅ ${objType} ${objName} - Syntax check passed`);
|
|
369
|
+
console.log(`✅ ${objType} ${objName}${fugrFmLabel} - Syntax check passed`);
|
|
272
370
|
if (warnings.length > 0) {
|
|
273
371
|
console.log(` (${warnings.length} warning(s))`);
|
|
274
372
|
}
|
|
275
373
|
} else {
|
|
276
|
-
console.log(`❌ ${objType} ${objName} - Syntax check failed (${errorCount} error(s))`);
|
|
374
|
+
console.log(`❌ ${objType} ${objName}${fugrFmLabel} - Syntax check failed (${errorCount} error(s))`);
|
|
277
375
|
console.log('');
|
|
278
376
|
console.log('Errors:');
|
|
279
377
|
console.log('─'.repeat(60));
|
|
@@ -287,19 +385,25 @@ module.exports = {
|
|
|
287
385
|
|
|
288
386
|
// Display which file/include the error is in
|
|
289
387
|
if (include) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
'testclasses': { display: 'Test classes', suffix: '.clas.testclasses.abap' }
|
|
295
|
-
};
|
|
296
|
-
const includeInfo = includeMap[include] || { display: include, suffix: '' };
|
|
297
|
-
|
|
298
|
-
// Show both display name and filename
|
|
299
|
-
if (includeInfo.suffix) {
|
|
300
|
-
console.log(` In: ${includeInfo.display} (${objName.toLowerCase()}${includeInfo.suffix})`);
|
|
388
|
+
// For FUGR: include = lowercase FM name → display as '<group>.fugr.<fm_name>.abap'
|
|
389
|
+
if (objType === 'FUGR') {
|
|
390
|
+
const fugrFile = `${objName.toLowerCase()}.fugr.${include}.abap`;
|
|
391
|
+
console.log(` In: Function module ${include.toUpperCase()} (${fugrFile})`);
|
|
301
392
|
} else {
|
|
302
|
-
|
|
393
|
+
const includeMap = {
|
|
394
|
+
'main': { display: 'Main class', suffix: '.clas.abap' },
|
|
395
|
+
'locals_def': { display: 'Local definitions', suffix: '.clas.locals_def.abap' },
|
|
396
|
+
'locals_imp': { display: 'Local implementations', suffix: '.clas.locals_imp.abap' },
|
|
397
|
+
'testclasses': { display: 'Test classes', suffix: '.clas.testclasses.abap' }
|
|
398
|
+
};
|
|
399
|
+
const includeInfo = includeMap[include] || { display: include, suffix: '' };
|
|
400
|
+
|
|
401
|
+
// Show both display name and filename
|
|
402
|
+
if (includeInfo.suffix) {
|
|
403
|
+
console.log(` In: ${includeInfo.display} (${objName.toLowerCase()}${includeInfo.suffix})`);
|
|
404
|
+
} else {
|
|
405
|
+
console.log(` In: ${includeInfo.display}`);
|
|
406
|
+
}
|
|
303
407
|
}
|
|
304
408
|
}
|
|
305
409
|
if (methodName) {
|