abapgit-agent 1.14.2 → 1.14.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abapgit-agent",
3
- "version": "1.14.2",
3
+ "version": "1.14.4",
4
4
  "description": "ABAP Git Agent - Pull and activate ABAP code via abapGit from any git repository",
5
5
  "files": [
6
6
  "bin/",
@@ -61,7 +61,8 @@ module.exports = {
61
61
  // Scope to changed files + their direct dependencies (interfaces, superclasses)
62
62
  // so abaplint can resolve cross-references without including the whole repo.
63
63
  const abapDir = cfg.global.files.replace(/\/\*\*.*$/, '').replace(/^\//, '') || 'abap';
64
- const depFiles = resolveDependencies(abapFiles, abapDir);
64
+ const fileIndex = buildFileIndex(abapDir);
65
+ const depFiles = resolveDependencies(abapFiles, fileIndex);
65
66
  const allFiles = [...new Set([...abapFiles, ...depFiles])];
66
67
  cfg.global.files = allFiles.map(f => `/${f}`);
67
68
 
@@ -69,15 +70,40 @@ module.exports = {
69
70
  fs.writeFileSync(scopedConfig, JSON.stringify(cfg, null, 2));
70
71
 
71
72
  // ── Run abaplint ──────────────────────────────────────────────────────────
73
+ // Dep files are included in the scoped config so abaplint can resolve
74
+ // cross-references (e.g. implement_methods needs the interface source).
75
+ // When producing checkstyle output (CI mode), post-filter the XML to only
76
+ // keep <file> blocks for the originally changed files — suppressing any
77
+ // pre-existing issues in dependency files that were not part of this change.
72
78
  try {
73
- const formatArgs = outformat ? `--outformat ${outformat}` : '';
74
- const fileArgs = outfile ? `--outfile ${outfile}` : '';
75
- const result = spawnSync(
76
- `npx @abaplint/cli@latest ${scopedConfig} ${formatArgs} ${fileArgs}`,
77
- { stdio: 'inherit', shell: true }
78
- );
79
- if (result.status !== 0) {
80
- process.exitCode = result.status;
79
+ if (outformat === 'checkstyle') {
80
+ // Run to a temp file, filter, then write to the final destination.
81
+ const tempOut = '.abaplint-raw.xml';
82
+ const abapFilesSet = new Set(abapFiles.map(f => path.resolve(f)));
83
+ try {
84
+ const result = spawnSync(
85
+ `npx @abaplint/cli@latest ${scopedConfig} --outformat checkstyle --outfile ${tempOut}`,
86
+ { stdio: 'pipe', shell: true }
87
+ );
88
+ const raw = fs.existsSync(tempOut) ? fs.readFileSync(tempOut, 'utf8') : '<checkstyle version="8.0"/>';
89
+ const filtered = filterCheckstyleToFiles(raw, abapFilesSet);
90
+ if (outfile) {
91
+ fs.writeFileSync(outfile, filtered);
92
+ } else {
93
+ process.stdout.write(filtered);
94
+ }
95
+ const issueCount = (filtered.match(/<error /g) || []).length;
96
+ if (issueCount > 0) process.exitCode = 1;
97
+ } finally {
98
+ if (fs.existsSync(tempOut)) fs.unlinkSync(tempOut);
99
+ }
100
+ } else {
101
+ // Interactive: inherit stdio so abaplint's human-readable output flows through.
102
+ const result = spawnSync(
103
+ `npx @abaplint/cli@latest ${scopedConfig}`,
104
+ { stdio: 'inherit', shell: true }
105
+ );
106
+ if (result.status !== 0) process.exitCode = result.status;
81
107
  }
82
108
  } finally {
83
109
  fs.unlinkSync(scopedConfig);
@@ -123,6 +149,30 @@ function runGit(cmd) {
123
149
  }
124
150
  }
125
151
 
152
+ /**
153
+ * Build a map of basename → full path for all .abap and .xml files
154
+ * found recursively under abapDir. Used for dependency resolution so
155
+ * that projects with nested folder structures (e.g. src/module/pkg/foo.clas.abap)
156
+ * are handled correctly — not just flat abap/ layouts.
157
+ */
158
+ function buildFileIndex(abapDir) {
159
+ const index = new Map(); // basename (lowercase) → full path
160
+ function walk(dir) {
161
+ let entries;
162
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
163
+ for (const entry of entries) {
164
+ const full = path.join(dir, entry.name);
165
+ if (entry.isDirectory()) {
166
+ walk(full);
167
+ } else if (entry.name.endsWith('.abap') || entry.name.endsWith('.xml')) {
168
+ index.set(entry.name.toLowerCase(), full);
169
+ }
170
+ }
171
+ }
172
+ walk(abapDir);
173
+ return index;
174
+ }
175
+
126
176
  /**
127
177
  * Resolve direct dependencies of the given ABAP files by scanning their source
128
178
  * for interface/superclass/type references and mapping them to local files.
@@ -132,17 +182,18 @@ function runGit(cmd) {
132
182
  * INHERITING FROM <name> → <name>.clas.abap + <name>.clas.xml
133
183
  * TYPE REF TO <name> → <name>.intf.abap or <name>.clas.abap (whichever exists)
134
184
  *
135
- * Only resolves one level deep enough for abaplint to check the changed files.
185
+ * Uses a filename index built from a recursive walk of abapDir so that
186
+ * deeply nested project structures are handled correctly.
136
187
  * XML companion files are always included alongside their .abap counterpart
137
188
  * so xml_consistency checks can run.
138
189
  */
139
- function resolveDependencies(abapFiles, abapDir) {
190
+ function resolveDependencies(abapFiles, fileIndex) {
140
191
  const deps = new Set();
141
192
  const visited = new Set(abapFiles); // don't re-scan changed files as deps
142
193
 
143
194
  // Patterns to extract referenced object names from ABAP source
144
195
  const patterns = [
145
- /^\s*INTERFACES\s+(\w+)\s*\./gim,
196
+ /^\s*INTERFACES:?\s+(\w+)\s*\./gim,
146
197
  /INHERITING\s+FROM\s+(\w+)/gim,
147
198
  /TYPE\s+REF\s+TO\s+(\w+)/gim,
148
199
  ];
@@ -162,11 +213,11 @@ function resolveDependencies(abapFiles, abapDir) {
162
213
  while ((match = pattern.exec(source)) !== null) {
163
214
  const name = match[1].toLowerCase();
164
215
  for (const suffix of [`${name}.intf`, `${name}.clas`]) {
165
- const abapFile = path.join(abapDir, `${suffix}.abap`);
166
- const xmlFile = path.join(abapDir, `${suffix}.xml`);
167
- if (fs.existsSync(abapFile)) {
216
+ const abapFile = fileIndex.get(`${suffix}.abap`);
217
+ const xmlFile = fileIndex.get(`${suffix}.xml`);
218
+ if (abapFile) {
168
219
  deps.add(abapFile);
169
- if (fs.existsSync(xmlFile)) deps.add(xmlFile);
220
+ if (xmlFile) deps.add(xmlFile);
170
221
  // Recurse into this dep if not yet visited
171
222
  if (!visited.has(abapFile)) {
172
223
  visited.add(abapFile);
@@ -175,12 +226,12 @@ function resolveDependencies(abapFiles, abapDir) {
175
226
  // For interfaces, also include the canonical concrete implementation
176
227
  // (zif_foo → zcl_foo) so rules like unused_variables can fully type-check.
177
228
  if (suffix.endsWith('.intf')) {
178
- const implName = name.replace(/^zif_/, 'zcl_');
179
- const implFile = path.join(abapDir, `${implName}.clas.abap`);
180
- const implXml = path.join(abapDir, `${implName}.clas.xml`);
181
- if (fs.existsSync(implFile)) {
229
+ const implName = name.replace(/^zif_/, 'zcl_');
230
+ const implFile = fileIndex.get(`${implName}.clas.abap`);
231
+ const implXml = fileIndex.get(`${implName}.clas.xml`);
232
+ if (implFile) {
182
233
  deps.add(implFile);
183
- if (fs.existsSync(implXml)) deps.add(implXml);
234
+ if (implXml) deps.add(implXml);
184
235
  if (!visited.has(implFile)) {
185
236
  visited.add(implFile);
186
237
  queue.push(implFile);
@@ -194,13 +245,38 @@ function resolveDependencies(abapFiles, abapDir) {
194
245
  }
195
246
 
196
247
  // Always include the XML companion of each scanned file
197
- const xmlCompanion = file.replace(/\.abap$/, '.xml');
198
- if (fs.existsSync(xmlCompanion)) deps.add(xmlCompanion);
248
+ const xmlBasename = path.basename(file).replace(/\.abap$/, '.xml').toLowerCase();
249
+ const xmlCompanion = fileIndex.get(xmlBasename);
250
+ if (xmlCompanion) deps.add(xmlCompanion);
199
251
  }
200
252
 
201
253
  return [...deps];
202
254
  }
203
255
 
256
+ /**
257
+ * Filter a checkstyle XML string to only include <file> blocks whose name
258
+ * attribute resolves to one of the files in the given Set of absolute paths.
259
+ * The outer <checkstyle> wrapper is preserved; the version attribute is kept.
260
+ */
261
+ function filterCheckstyleToFiles(xml, abapFilesSet) {
262
+ // Extract the opening <checkstyle ...> tag (preserves version= attribute).
263
+ const headerMatch = xml.match(/^[\s\S]*?(<checkstyle[^>]*>)/);
264
+ const header = headerMatch ? headerMatch[1] : '<checkstyle version="8.0">';
265
+
266
+ // Match each <file name="...">...</file> block (including self-closing).
267
+ const fileBlockRe = /<file\s+name="([^"]*)"[\s\S]*?<\/file>|<file\s+name="([^"]*)"\s*\/>/g;
268
+ const kept = [];
269
+ let match;
270
+ while ((match = fileBlockRe.exec(xml)) !== null) {
271
+ const filePath = match[1] || match[2];
272
+ if (abapFilesSet.has(path.resolve(filePath))) {
273
+ kept.push(match[0]);
274
+ }
275
+ }
276
+
277
+ return `<?xml version="1.0" encoding="UTF-8"?>\n${header}\n${kept.join('\n')}${kept.length ? '\n' : ''}</checkstyle>\n`;
278
+ }
279
+
204
280
  /**
205
281
  * Keep only files that look like ABAP source files
206
282
  * (name.type.abap or name.type.subtype.abap).