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 +1 -1
- package/src/commands/lint.js +99 -23
package/package.json
CHANGED
package/src/commands/lint.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
*
|
|
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,
|
|
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
|
|
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 =
|
|
166
|
-
const xmlFile =
|
|
167
|
-
if (
|
|
216
|
+
const abapFile = fileIndex.get(`${suffix}.abap`);
|
|
217
|
+
const xmlFile = fileIndex.get(`${suffix}.xml`);
|
|
218
|
+
if (abapFile) {
|
|
168
219
|
deps.add(abapFile);
|
|
169
|
-
if (
|
|
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
|
|
179
|
-
const implFile
|
|
180
|
-
const implXml
|
|
181
|
-
if (
|
|
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 (
|
|
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
|
|
198
|
-
|
|
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).
|