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.
@@ -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 | `$` | `$MY_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.15.2",
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
+ };
@@ -30,5 +30,6 @@ module.exports = {
30
30
  pull: require('./pull'),
31
31
  upgrade: require('./upgrade'),
32
32
  transport: require('./transport'),
33
- lint: require('./lint')
33
+ lint: require('./lint'),
34
+ drop: require('./drop')
34
35
  };
@@ -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 (const res of results) {
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
- const includeMap = {
291
- 'main': { display: 'Main class', suffix: '.clas.abap' },
292
- 'locals_def': { display: 'Local definitions', suffix: '.clas.locals_def.abap' },
293
- 'locals_imp': { display: 'Local implementations', suffix: '.clas.locals_imp.abap' },
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
- console.log(` In: ${includeInfo.display}`);
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) {