test-pmd-rule 0.1.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/LICENSE.md +21 -0
- package/README.md +232 -0
- package/dist/test-pmd-rule.js +2197 -0
- package/dist/test-pmd-rule.js.map +7 -0
- package/package.json +54 -0
|
@@ -0,0 +1,2197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/main.ts
|
|
4
|
+
import { existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
5
|
+
import { resolve as resolve3, extname } from "path";
|
|
6
|
+
import { argv } from "process";
|
|
7
|
+
import { cpus } from "os";
|
|
8
|
+
|
|
9
|
+
// src/tester/RuleTester.ts
|
|
10
|
+
import { readFileSync as readFileSync3, existsSync } from "fs";
|
|
11
|
+
import { DOMParser as DOMParser3 } from "@xmldom/xmldom";
|
|
12
|
+
|
|
13
|
+
// src/xpath/extractXPath.ts
|
|
14
|
+
import { readFileSync, realpathSync } from "fs";
|
|
15
|
+
import { resolve } from "path";
|
|
16
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
17
|
+
var FIRST_ELEMENT_INDEX = 0;
|
|
18
|
+
var MIN_STRING_LENGTH = 0;
|
|
19
|
+
function normalizePath(filePath) {
|
|
20
|
+
const resolvedPath = resolve(filePath);
|
|
21
|
+
const canonicalPath = realpathSync(resolvedPath);
|
|
22
|
+
return canonicalPath;
|
|
23
|
+
}
|
|
24
|
+
function extractXPath(xmlFilePath) {
|
|
25
|
+
try {
|
|
26
|
+
const normalizedPath = normalizePath(xmlFilePath);
|
|
27
|
+
const content = readFileSync(normalizedPath, "utf-8");
|
|
28
|
+
const parser = new DOMParser();
|
|
29
|
+
const doc = parser.parseFromString(content, "text/xml");
|
|
30
|
+
const properties = doc.getElementsByTagName("properties")[FIRST_ELEMENT_INDEX];
|
|
31
|
+
if (!properties) {
|
|
32
|
+
return { data: null, success: true };
|
|
33
|
+
}
|
|
34
|
+
const xpathProperty = Array.from(
|
|
35
|
+
properties.getElementsByTagName("property")
|
|
36
|
+
).find(
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Element is used for getAttribute
|
|
38
|
+
(prop) => prop.getAttribute("name") === "xpath"
|
|
39
|
+
);
|
|
40
|
+
if (!xpathProperty) {
|
|
41
|
+
return { data: null, success: true };
|
|
42
|
+
}
|
|
43
|
+
const valueElement = xpathProperty.getElementsByTagName("value")[FIRST_ELEMENT_INDEX];
|
|
44
|
+
if (!valueElement) {
|
|
45
|
+
return { data: null, success: true };
|
|
46
|
+
}
|
|
47
|
+
const { textContent } = valueElement;
|
|
48
|
+
const trimmed = textContent !== null ? textContent.trim() : null;
|
|
49
|
+
const hasContent = trimmed !== null && trimmed.length > MIN_STRING_LENGTH;
|
|
50
|
+
const xpath = hasContent ? trimmed : null;
|
|
51
|
+
return { data: xpath, success: true };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
54
|
+
return {
|
|
55
|
+
error: `Error extracting XPath: ${errorMessage}`,
|
|
56
|
+
success: false
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/xpath/checkCoverage.ts
|
|
62
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
63
|
+
|
|
64
|
+
// src/xpath/extractors/extractNodeTypes.ts
|
|
65
|
+
function extractNodeTypes(xpath) {
|
|
66
|
+
const MIN_STRING_LENGTH5 = 0;
|
|
67
|
+
if (xpath.length === MIN_STRING_LENGTH5) return [];
|
|
68
|
+
const nodeTypes = /* @__PURE__ */ new Set();
|
|
69
|
+
const nodeTypeMatches1 = xpath.matchAll(
|
|
70
|
+
/(?:\.\/\/|\/\/|\s|\/|\(|\[|,|\|)([A-Z][a-zA-Z]*(?:Statement|Expression|Declaration|Node|Block))(?=\s|$|\[|\(|\/|\)|,|\||]|or|and|not|return|let)/g
|
|
71
|
+
);
|
|
72
|
+
const nodeTypeMatches2 = xpath.matchAll(
|
|
73
|
+
/(?:\.\/\/|\/\/|\s|\/|\(|\[|,|\|)(Method|Field|Class|Type|Condition|Loop|Block|Parameter|Property|Annotation)(?=\s|$|\[|\(|\/|\)|,|\||]|or|and|not|return|let)/g
|
|
74
|
+
);
|
|
75
|
+
const nodeTypeMatches3 = xpath.matchAll(
|
|
76
|
+
/(?:\.\/\/|\/\/|\s|\/|\(|\[|,|\|)([A-Z][a-zA-Z]*(?:Method|Class|Field|Condition|Loop|Type)[a-zA-Z]*)(?=\s|$|\[|\(|\/|\)|,|\||]|or|and|not|return|let)/g
|
|
77
|
+
);
|
|
78
|
+
const nodeTypeMatches4 = xpath.matchAll(
|
|
79
|
+
/(?:\.\/\/|\/\/|\s|\/|\(|\[|,|\|)(ApexFile|CompilationUnit)(?=\s|$|\[|\(|\/|\)|,|\||]|or|and|not|return|let)/g
|
|
80
|
+
);
|
|
81
|
+
const nodeTypeMatches5 = xpath.matchAll(
|
|
82
|
+
/(?:\.\/\/|\/\/|\s|\/|\(|\[|,|\|)(UserClass|UserInterface|UserEnum)(?=\s|$|\[|\(|\/|\)|,|\||]|or|and|not|return|let)/g
|
|
83
|
+
);
|
|
84
|
+
const nodeTypeMatches6 = xpath.matchAll(
|
|
85
|
+
/(?:\.\/\/|\/\/|\s|\/|\(|\[|,|\|)(Dml(?:Insert|Update|Delete|Undelete|Upsert|Merge)Statement)(?=\s|$|\[|\(|\/|\)|,|\||]|or|and|not|return|let)/g
|
|
86
|
+
);
|
|
87
|
+
const nodeTypeMatches7 = xpath.matchAll(
|
|
88
|
+
/(?:\.\/\/|\/\/|\s|\/|\(|\[|,|\|)((?:Constructor|Values|Map|SizedArray)Initializer)(?=\s|$|\[|\(|\/|\)|,|\||]|or|and|not|return|let)/g
|
|
89
|
+
);
|
|
90
|
+
const nodeTypeMatches8 = xpath.matchAll(
|
|
91
|
+
/(?:\.\/\/|\/\/|\s|\/|\(|\[|,|\|)(ModifierNode|KeywordModifier)(?=\s|$|\[|\(|\/|\)|,|\||]|or|and|not|return|let)/g
|
|
92
|
+
);
|
|
93
|
+
const nodeTypeMatches9 = xpath.matchAll(
|
|
94
|
+
/(?:\.\/\/|\/\/|\s|\/|\(|\[|,|\|)(TypeRef|Identifier|StandardCondition|WhenValue|WhenType|WhenElse|EnumValue|AnnotationParameter|SoqlOrSoslBinding|FormalComment|MapEntryNode|SuperExpression|ThisVariableExpression|CompoundStatement|BreakStatement|ContinueStatement|EmptyStatement|TriggerDeclaration)(?=\s|$|\[|\(|\/|\)|,|\||]|or|and|not|return|let)/g
|
|
95
|
+
);
|
|
96
|
+
const nodeTypeMatches10 = xpath.matchAll(
|
|
97
|
+
/(?:\.\/\/|\/\/|\s|\/|\(|\[|,|\|)(SoqlExpression|SoslExpression)(?=\s|$|\[|\(|\/|\)|,|\||]|or|and|not|return|let)/g
|
|
98
|
+
);
|
|
99
|
+
const MATCH_INDEX3 = 1;
|
|
100
|
+
const allMatches = [
|
|
101
|
+
nodeTypeMatches1,
|
|
102
|
+
nodeTypeMatches2,
|
|
103
|
+
nodeTypeMatches3,
|
|
104
|
+
nodeTypeMatches4,
|
|
105
|
+
nodeTypeMatches5,
|
|
106
|
+
nodeTypeMatches6,
|
|
107
|
+
nodeTypeMatches7,
|
|
108
|
+
nodeTypeMatches8,
|
|
109
|
+
nodeTypeMatches9,
|
|
110
|
+
nodeTypeMatches10
|
|
111
|
+
];
|
|
112
|
+
for (const matches of allMatches) {
|
|
113
|
+
for (const match of matches) {
|
|
114
|
+
const nodeType = match[MATCH_INDEX3];
|
|
115
|
+
nodeTypes.add(nodeType);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return Array.from(nodeTypes);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/xpath/extractors/extractOperators.ts
|
|
122
|
+
var MATCH_INDEX = 1;
|
|
123
|
+
var MIN_STRING_LENGTH2 = 0;
|
|
124
|
+
function extractOperators(xpath) {
|
|
125
|
+
if (xpath.length === MIN_STRING_LENGTH2) return [];
|
|
126
|
+
const operators = /* @__PURE__ */ new Set();
|
|
127
|
+
const opMatches = xpath.matchAll(/@Op\s*=\s*['"]([^'"]+)['"]/g);
|
|
128
|
+
for (const match of opMatches) {
|
|
129
|
+
const operator = match[MATCH_INDEX];
|
|
130
|
+
operators.add(operator);
|
|
131
|
+
}
|
|
132
|
+
return Array.from(operators);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/xpath/extractors/extractAttributes.ts
|
|
136
|
+
var MATCH_INDEX2 = 1;
|
|
137
|
+
var MIN_STRING_LENGTH3 = 0;
|
|
138
|
+
function extractAttributes(xpath) {
|
|
139
|
+
if (xpath.length === MIN_STRING_LENGTH3) return [];
|
|
140
|
+
const attributes = /* @__PURE__ */ new Set();
|
|
141
|
+
const attrMatches = xpath.matchAll(
|
|
142
|
+
/@([A-Za-z][A-Za-z0-9]*)(?=\s|$|[\]|\)|,|\||=])/g
|
|
143
|
+
);
|
|
144
|
+
for (const match of attrMatches) {
|
|
145
|
+
const attr = match[MATCH_INDEX2];
|
|
146
|
+
if (attr !== void 0 && attr.length > MIN_STRING_LENGTH3 && attr !== "Op") {
|
|
147
|
+
attributes.add(attr);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return Array.from(attributes);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/xpath/extractors/extractConditionals.ts
|
|
154
|
+
function extractConditionals(xpath) {
|
|
155
|
+
const MIN_STRING_LENGTH5 = 0;
|
|
156
|
+
if (xpath.length === MIN_STRING_LENGTH5) return [];
|
|
157
|
+
const conditionals = [];
|
|
158
|
+
const MATCH_INDEX3 = 1;
|
|
159
|
+
const notMatches = xpath.matchAll(/not\s*\(([^)]+)\)/g);
|
|
160
|
+
for (const match of notMatches) {
|
|
161
|
+
const expression = match[MATCH_INDEX3];
|
|
162
|
+
const position = match.index;
|
|
163
|
+
conditionals.push({
|
|
164
|
+
expression: expression.trim(),
|
|
165
|
+
position,
|
|
166
|
+
type: "not"
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const andMatches = xpath.matchAll(/and\s+([^[\]]+)(?=\s*\])/g);
|
|
170
|
+
for (const match of andMatches) {
|
|
171
|
+
const expression = match[MATCH_INDEX3];
|
|
172
|
+
const position = match.index;
|
|
173
|
+
conditionals.push({
|
|
174
|
+
expression: expression.trim(),
|
|
175
|
+
position,
|
|
176
|
+
type: "and"
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const orMatches = xpath.matchAll(/or\s+([^[\]]+)(?=\s*\])/g);
|
|
180
|
+
for (const match of orMatches) {
|
|
181
|
+
const expression = match[MATCH_INDEX3];
|
|
182
|
+
const position = match.index;
|
|
183
|
+
conditionals.push({
|
|
184
|
+
expression: expression.trim(),
|
|
185
|
+
position,
|
|
186
|
+
type: "or"
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return conditionals;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/xpath/analyzeXPath.ts
|
|
193
|
+
function analyzeXPath(xpath) {
|
|
194
|
+
if (!xpath) {
|
|
195
|
+
return {
|
|
196
|
+
attributes: [],
|
|
197
|
+
conditionals: [],
|
|
198
|
+
hasLetExpressions: false,
|
|
199
|
+
hasUnions: false,
|
|
200
|
+
nodeTypes: [],
|
|
201
|
+
operators: [],
|
|
202
|
+
patterns: []
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const nodeTypes = extractNodeTypes(xpath);
|
|
206
|
+
const operators = extractOperators(xpath);
|
|
207
|
+
const attributes = extractAttributes(xpath);
|
|
208
|
+
const conditionals = extractConditionals(xpath);
|
|
209
|
+
const hasUnions = xpath.includes("|");
|
|
210
|
+
const hasLetExpressions = xpath.includes("let ");
|
|
211
|
+
return {
|
|
212
|
+
attributes,
|
|
213
|
+
conditionals,
|
|
214
|
+
hasLetExpressions,
|
|
215
|
+
hasUnions,
|
|
216
|
+
nodeTypes,
|
|
217
|
+
operators,
|
|
218
|
+
patterns: []
|
|
219
|
+
// Reserved for future pattern analysis
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/xpath/checkCoverage.ts
|
|
224
|
+
var MIN_COUNT = 0;
|
|
225
|
+
var NOT_FOUND_INDEX = -1;
|
|
226
|
+
var LINE_OFFSET = 1;
|
|
227
|
+
function findAttributeLineNumber(ruleFilePath, xpath, attribute) {
|
|
228
|
+
try {
|
|
229
|
+
const content = readFileSync2(ruleFilePath, "utf-8");
|
|
230
|
+
const lines = content.split("\n");
|
|
231
|
+
const attributePattern = `@${attribute}`;
|
|
232
|
+
for (let i = 0; i < lines.length; i++) {
|
|
233
|
+
const line = lines[i];
|
|
234
|
+
const hasXPath = line.includes("xpath");
|
|
235
|
+
const hasValue = line.includes("value");
|
|
236
|
+
const hasAttribute = line.includes(attributePattern);
|
|
237
|
+
if (hasXPath && hasValue && hasAttribute) {
|
|
238
|
+
return i + LINE_OFFSET;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
let inXPathSection = false;
|
|
242
|
+
for (let i = 0; i < lines.length; i++) {
|
|
243
|
+
const line = lines[i];
|
|
244
|
+
if (line.includes("<property") && line.includes('name="xpath"')) {
|
|
245
|
+
inXPathSection = true;
|
|
246
|
+
}
|
|
247
|
+
if (inXPathSection && line.includes(attributePattern)) {
|
|
248
|
+
return i + LINE_OFFSET;
|
|
249
|
+
}
|
|
250
|
+
if (inXPathSection && line.includes("</property>")) {
|
|
251
|
+
inXPathSection = false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const xpathIndex = xpath.indexOf(attributePattern);
|
|
255
|
+
if (xpathIndex !== NOT_FOUND_INDEX) {
|
|
256
|
+
for (let i = 0; i < lines.length; i++) {
|
|
257
|
+
const line = lines[i];
|
|
258
|
+
if (line.includes("<value>")) {
|
|
259
|
+
const xpathBeforeAttribute = xpath.substring(
|
|
260
|
+
MIN_COUNT,
|
|
261
|
+
xpathIndex
|
|
262
|
+
);
|
|
263
|
+
const newlineMatches = xpathBeforeAttribute.match(/\n/g);
|
|
264
|
+
const newlineCount = newlineMatches ? newlineMatches.length : MIN_COUNT;
|
|
265
|
+
return i + LINE_OFFSET + newlineCount;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function findNodeTypeLineNumber(ruleFilePath, xpath, nodeType) {
|
|
275
|
+
try {
|
|
276
|
+
const content = readFileSync2(ruleFilePath, "utf-8");
|
|
277
|
+
const lines = content.split("\n");
|
|
278
|
+
for (let i = 0; i < lines.length; i++) {
|
|
279
|
+
const line = lines[i];
|
|
280
|
+
const hasXPath = line.includes("xpath");
|
|
281
|
+
const hasValue = line.includes("value");
|
|
282
|
+
const hasNodeType = line.includes(nodeType);
|
|
283
|
+
if (hasXPath && hasValue && hasNodeType) {
|
|
284
|
+
return i + LINE_OFFSET;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
let inXPathSection = false;
|
|
288
|
+
for (let i = 0; i < lines.length; i++) {
|
|
289
|
+
const line = lines[i];
|
|
290
|
+
if (line.includes("<property") && line.includes('name="xpath"')) {
|
|
291
|
+
inXPathSection = true;
|
|
292
|
+
}
|
|
293
|
+
if (inXPathSection && line.includes(nodeType)) {
|
|
294
|
+
return i + LINE_OFFSET;
|
|
295
|
+
}
|
|
296
|
+
if (inXPathSection && line.includes("</property>")) {
|
|
297
|
+
inXPathSection = false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const xpathIndex = xpath.indexOf(nodeType);
|
|
301
|
+
if (xpathIndex !== NOT_FOUND_INDEX) {
|
|
302
|
+
for (let i = 0; i < lines.length; i++) {
|
|
303
|
+
const line = lines[i];
|
|
304
|
+
if (line.includes("<value>")) {
|
|
305
|
+
const xpathBeforeNodeType = xpath.substring(
|
|
306
|
+
MIN_COUNT,
|
|
307
|
+
xpathIndex
|
|
308
|
+
);
|
|
309
|
+
const newlineMatches = xpathBeforeNodeType.match(/\n/g);
|
|
310
|
+
const newlineCount = newlineMatches ? newlineMatches.length : MIN_COUNT;
|
|
311
|
+
return i + LINE_OFFSET + newlineCount;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function checkNodeTypeCoverage(nodeTypes, content, options) {
|
|
321
|
+
const lineNumberCollector = options?.lineNumberCollector;
|
|
322
|
+
const lowerContent = content.toLowerCase();
|
|
323
|
+
const foundNodeTypes = [];
|
|
324
|
+
const missingNodeTypes = [];
|
|
325
|
+
for (const nodeType of nodeTypes) {
|
|
326
|
+
let isCovered = false;
|
|
327
|
+
switch (nodeType) {
|
|
328
|
+
case "BinaryExpression":
|
|
329
|
+
isCovered = /[+\-*/=<>!&|]{1,2}/.test(content);
|
|
330
|
+
break;
|
|
331
|
+
case "LiteralExpression":
|
|
332
|
+
isCovered = /\b\d+(\.\d+)?\b|'(?:[^'\\]|\\.)*'|"[^"]*"|\bnull\b|\btrue\b|\bfalse\b/.test(
|
|
333
|
+
lowerContent
|
|
334
|
+
);
|
|
335
|
+
break;
|
|
336
|
+
case "ModifierNode":
|
|
337
|
+
isCovered = /\b(static|final|public|private|protected)\b/.test(
|
|
338
|
+
lowerContent
|
|
339
|
+
);
|
|
340
|
+
break;
|
|
341
|
+
case "Annotation":
|
|
342
|
+
isCovered = /@\w+/.test(content);
|
|
343
|
+
break;
|
|
344
|
+
case "AnnotationParameter":
|
|
345
|
+
isCovered = /@\w+\([^)]+\)/.test(content);
|
|
346
|
+
break;
|
|
347
|
+
case "IfBlockStatement":
|
|
348
|
+
isCovered = /\bif\b/.test(lowerContent);
|
|
349
|
+
break;
|
|
350
|
+
case "SwitchStatement":
|
|
351
|
+
isCovered = /\bswitch\b/.test(lowerContent);
|
|
352
|
+
break;
|
|
353
|
+
case "ForLoopStatement":
|
|
354
|
+
isCovered = /\bfor\s*\(/.test(lowerContent);
|
|
355
|
+
break;
|
|
356
|
+
case "ForEachStatement":
|
|
357
|
+
isCovered = /\bfor\s*\([^:]+:[^)]+\)/.test(lowerContent);
|
|
358
|
+
break;
|
|
359
|
+
case "WhileLoopStatement":
|
|
360
|
+
isCovered = /\bwhile\b/.test(lowerContent);
|
|
361
|
+
break;
|
|
362
|
+
case "DoWhileLoopStatement":
|
|
363
|
+
isCovered = /\bdo\b/.test(lowerContent);
|
|
364
|
+
break;
|
|
365
|
+
case "TernaryExpression":
|
|
366
|
+
isCovered = /\?\s*[^:]+\s*:\s*[^;]+/.test(content);
|
|
367
|
+
break;
|
|
368
|
+
case "MethodCallExpression":
|
|
369
|
+
isCovered = /\w+\s*\(/.test(content);
|
|
370
|
+
break;
|
|
371
|
+
case "StandardCondition":
|
|
372
|
+
isCovered = true;
|
|
373
|
+
break;
|
|
374
|
+
default:
|
|
375
|
+
isCovered = lowerContent.includes(nodeType.toLowerCase());
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
if (isCovered) {
|
|
379
|
+
foundNodeTypes.push(nodeType);
|
|
380
|
+
} else {
|
|
381
|
+
missingNodeTypes.push(nodeType);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const missingList = missingNodeTypes.length > MIN_COUNT ? missingNodeTypes.map((item) => {
|
|
385
|
+
if (options !== void 0) {
|
|
386
|
+
const ruleFilePathValue = options.ruleFilePath;
|
|
387
|
+
const xpathValue = options.xpath;
|
|
388
|
+
const lineNumber = findNodeTypeLineNumber(
|
|
389
|
+
ruleFilePathValue,
|
|
390
|
+
xpathValue,
|
|
391
|
+
item
|
|
392
|
+
);
|
|
393
|
+
if (lineNumber !== null && lineNumberCollector) {
|
|
394
|
+
lineNumberCollector(lineNumber);
|
|
395
|
+
}
|
|
396
|
+
return lineNumber !== null ? ` - Line ${String(lineNumber)}: ${item}` : ` - ${item}`;
|
|
397
|
+
}
|
|
398
|
+
return ` - ${item}`;
|
|
399
|
+
}).join("\n") : "";
|
|
400
|
+
const missingText = missingNodeTypes.length > MIN_COUNT ? `Missing:
|
|
401
|
+
${missingList}` : "";
|
|
402
|
+
const description = missingText.length > MIN_COUNT ? missingText : "";
|
|
403
|
+
return {
|
|
404
|
+
count: foundNodeTypes.length,
|
|
405
|
+
description,
|
|
406
|
+
required: nodeTypes.length,
|
|
407
|
+
type: "violation"
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
var MAX_EXPRESSION_LENGTH = 50;
|
|
411
|
+
function truncateExpression(expression, maxLength) {
|
|
412
|
+
const normalized = expression.replace(/\s+/g, " ").trim();
|
|
413
|
+
if (normalized.length <= maxLength) {
|
|
414
|
+
return normalized;
|
|
415
|
+
}
|
|
416
|
+
return `${normalized.substring(MIN_COUNT, maxLength)}...`;
|
|
417
|
+
}
|
|
418
|
+
function checkConditionalCoverage(conditionals, content) {
|
|
419
|
+
const lowerContent = content.toLowerCase();
|
|
420
|
+
const foundConditionals = [];
|
|
421
|
+
const missingConditionals = [];
|
|
422
|
+
for (const conditional of conditionals) {
|
|
423
|
+
const exprLower = conditional.expression.toLowerCase();
|
|
424
|
+
let isCovered = false;
|
|
425
|
+
isCovered = lowerContent.includes(exprLower) || lowerContent.includes("if");
|
|
426
|
+
if (isCovered) {
|
|
427
|
+
const displayExpr = truncateExpression(
|
|
428
|
+
conditional.expression,
|
|
429
|
+
MAX_EXPRESSION_LENGTH
|
|
430
|
+
);
|
|
431
|
+
foundConditionals.push(` - ${conditional.type}: ${displayExpr}`);
|
|
432
|
+
} else {
|
|
433
|
+
const displayExpr = truncateExpression(
|
|
434
|
+
conditional.expression,
|
|
435
|
+
MAX_EXPRESSION_LENGTH
|
|
436
|
+
);
|
|
437
|
+
missingConditionals.push(` - ${conditional.type}: ${displayExpr}`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const missingList = missingConditionals.length > MIN_COUNT ? missingConditionals : [];
|
|
441
|
+
const missingText = missingList.length > MIN_COUNT ? `Missing:
|
|
442
|
+
${missingList.join("\n")}` : "";
|
|
443
|
+
const description = missingText.length > MIN_COUNT ? missingText : "";
|
|
444
|
+
return {
|
|
445
|
+
count: foundConditionals.length,
|
|
446
|
+
description,
|
|
447
|
+
required: conditionals.length,
|
|
448
|
+
type: "violation"
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function checkAttributeCoverage(attributes, content, options) {
|
|
452
|
+
const lineNumberCollector = options?.lineNumberCollector;
|
|
453
|
+
const lowerContent = content.toLowerCase();
|
|
454
|
+
const foundAttributes = [];
|
|
455
|
+
const missingAttributes = [];
|
|
456
|
+
for (const attr of attributes) {
|
|
457
|
+
let isCovered = false;
|
|
458
|
+
switch (attr) {
|
|
459
|
+
case "String":
|
|
460
|
+
isCovered = /'(?:[^'\\]|\\.)*'|"[^"]*"/.test(content);
|
|
461
|
+
break;
|
|
462
|
+
case "Null":
|
|
463
|
+
isCovered = /\bnull\b/.test(lowerContent);
|
|
464
|
+
break;
|
|
465
|
+
case "LiteralType":
|
|
466
|
+
isCovered = /\b\d+(\.\d+)?\b/.test(content);
|
|
467
|
+
break;
|
|
468
|
+
case "Image":
|
|
469
|
+
isCovered = /\b\d+(\.\d+)?\b|'(?:[^'\\]|\\.)*'|"[^"]*"|\bnull\b|\btrue\b|\bfalse\b/.test(
|
|
470
|
+
lowerContent
|
|
471
|
+
);
|
|
472
|
+
break;
|
|
473
|
+
case "Static":
|
|
474
|
+
isCovered = /\bstatic\b/.test(lowerContent);
|
|
475
|
+
break;
|
|
476
|
+
case "Final":
|
|
477
|
+
isCovered = /\bfinal\b/.test(lowerContent);
|
|
478
|
+
break;
|
|
479
|
+
case "Name":
|
|
480
|
+
isCovered = /@\w+\([^)]*\w+\s*=/.test(content) || lowerContent.includes(attr.toLowerCase());
|
|
481
|
+
break;
|
|
482
|
+
case "MethodName":
|
|
483
|
+
isCovered = /\w+\.\w+\s*\(|\w+\s*\(/.test(content);
|
|
484
|
+
break;
|
|
485
|
+
case "Value":
|
|
486
|
+
isCovered = /@\w+\([^)]*=\s*[^)]+\)/.test(content);
|
|
487
|
+
break;
|
|
488
|
+
default:
|
|
489
|
+
isCovered = lowerContent.includes(attr.toLowerCase());
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
if (isCovered) {
|
|
493
|
+
foundAttributes.push(attr);
|
|
494
|
+
} else {
|
|
495
|
+
missingAttributes.push(attr);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const missingList = missingAttributes.length > MIN_COUNT ? missingAttributes.map((item) => {
|
|
499
|
+
const ruleFilePath = options?.ruleFilePath;
|
|
500
|
+
const xpathValue = options?.xpath;
|
|
501
|
+
const hasOptions = ruleFilePath !== void 0 && xpathValue !== void 0;
|
|
502
|
+
if (hasOptions) {
|
|
503
|
+
const lineNumber = findAttributeLineNumber(
|
|
504
|
+
ruleFilePath,
|
|
505
|
+
xpathValue,
|
|
506
|
+
item
|
|
507
|
+
);
|
|
508
|
+
if (lineNumber !== null && lineNumberCollector) {
|
|
509
|
+
lineNumberCollector(lineNumber);
|
|
510
|
+
}
|
|
511
|
+
return lineNumber !== null ? ` - Line ${String(lineNumber)}: ${item}` : ` - ${item}`;
|
|
512
|
+
}
|
|
513
|
+
return ` - ${item}`;
|
|
514
|
+
}).join("\n") : "";
|
|
515
|
+
const missingText = missingAttributes.length > MIN_COUNT ? `Missing:
|
|
516
|
+
${missingList}` : "";
|
|
517
|
+
const description = missingText.length > MIN_COUNT ? missingText : "";
|
|
518
|
+
return {
|
|
519
|
+
count: foundAttributes.length,
|
|
520
|
+
description,
|
|
521
|
+
required: attributes.length,
|
|
522
|
+
type: "violation"
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
function checkOperatorCoverage(operators, content) {
|
|
526
|
+
const lowerContent = content.toLowerCase();
|
|
527
|
+
const foundOperators = [];
|
|
528
|
+
const missingOperators = [];
|
|
529
|
+
for (const op of operators) {
|
|
530
|
+
const opLower = op.toLowerCase();
|
|
531
|
+
if (lowerContent.includes(opLower)) {
|
|
532
|
+
foundOperators.push(op);
|
|
533
|
+
} else {
|
|
534
|
+
missingOperators.push(op);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
const foundList = foundOperators.length > MIN_COUNT ? foundOperators.map((item) => ` - ${item}`).join("\n") : "";
|
|
538
|
+
const missingList = missingOperators.length > MIN_COUNT ? missingOperators.map((item) => ` - ${item}`).join("\n") : "";
|
|
539
|
+
const foundText = foundList;
|
|
540
|
+
const missingText = missingOperators.length > MIN_COUNT ? `Missing:
|
|
541
|
+
${missingList}` : "";
|
|
542
|
+
const hasFound = foundText.length > MIN_COUNT;
|
|
543
|
+
const hasMissing = missingText.length > MIN_COUNT;
|
|
544
|
+
let description = "";
|
|
545
|
+
if (hasFound) {
|
|
546
|
+
description = hasMissing ? `${foundText}
|
|
547
|
+
${missingText}` : foundText;
|
|
548
|
+
}
|
|
549
|
+
if (!hasFound && hasMissing) {
|
|
550
|
+
description = missingText;
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
count: foundOperators.length,
|
|
554
|
+
description,
|
|
555
|
+
required: operators.length,
|
|
556
|
+
type: "violation"
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
function checkXPathCoverage(xpath, examples, ruleFilePath) {
|
|
560
|
+
const hasXPath = xpath !== null && xpath !== void 0 && xpath.length > MIN_COUNT;
|
|
561
|
+
if (!hasXPath || examples.length === MIN_COUNT) {
|
|
562
|
+
return {
|
|
563
|
+
coverage: [],
|
|
564
|
+
overallSuccess: false,
|
|
565
|
+
uncoveredBranches: []
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
const analysis = analyzeXPath(xpath);
|
|
569
|
+
const allContent = examples.map((ex) => ex.content).join("\n").toLowerCase();
|
|
570
|
+
const coverageResults = [];
|
|
571
|
+
const uncoveredBranches = [];
|
|
572
|
+
const coveredLineNumbers = /* @__PURE__ */ new Set();
|
|
573
|
+
if (analysis.nodeTypes.length > MIN_COUNT) {
|
|
574
|
+
const ruleFilePathValue = ruleFilePath;
|
|
575
|
+
const xpathValue = xpath;
|
|
576
|
+
const hasRuleFilePath = ruleFilePathValue !== void 0 && ruleFilePathValue.length > MIN_COUNT;
|
|
577
|
+
const hasXpathValue = xpathValue.length > MIN_COUNT;
|
|
578
|
+
const nodeTypeOptions = hasRuleFilePath && hasXpathValue ? {
|
|
579
|
+
lineNumberCollector: (lineNumber) => {
|
|
580
|
+
coveredLineNumbers.add(lineNumber);
|
|
581
|
+
},
|
|
582
|
+
ruleFilePath: ruleFilePathValue,
|
|
583
|
+
xpath: xpathValue
|
|
584
|
+
} : void 0;
|
|
585
|
+
const nodeTypeEvidence = checkNodeTypeCoverage(
|
|
586
|
+
analysis.nodeTypes,
|
|
587
|
+
allContent,
|
|
588
|
+
nodeTypeOptions
|
|
589
|
+
);
|
|
590
|
+
const nodeTypeSuccess = nodeTypeEvidence.count >= nodeTypeEvidence.required;
|
|
591
|
+
coverageResults.push({
|
|
592
|
+
details: [],
|
|
593
|
+
evidence: [nodeTypeEvidence],
|
|
594
|
+
message: `Node types: ${String(nodeTypeEvidence.count)}/${String(analysis.nodeTypes.length)} covered`,
|
|
595
|
+
success: nodeTypeSuccess
|
|
596
|
+
});
|
|
597
|
+
if (!nodeTypeSuccess) {
|
|
598
|
+
uncoveredBranches.push(
|
|
599
|
+
`Node types: ${analysis.nodeTypes.join(", ")}`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (analysis.conditionals.length > MIN_COUNT) {
|
|
604
|
+
const conditionalEvidence = checkConditionalCoverage(
|
|
605
|
+
analysis.conditionals,
|
|
606
|
+
allContent
|
|
607
|
+
);
|
|
608
|
+
const conditionalSuccess = conditionalEvidence.count >= conditionalEvidence.required;
|
|
609
|
+
coverageResults.push({
|
|
610
|
+
details: [],
|
|
611
|
+
evidence: [conditionalEvidence],
|
|
612
|
+
message: `Conditionals: ${String(conditionalEvidence.count)}/${String(analysis.conditionals.length)} covered`,
|
|
613
|
+
success: conditionalSuccess
|
|
614
|
+
});
|
|
615
|
+
if (!conditionalSuccess) {
|
|
616
|
+
uncoveredBranches.push(
|
|
617
|
+
`Conditionals: ${analysis.conditionals.map((c) => c.expression).join(", ")}`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (analysis.attributes.length > MIN_COUNT) {
|
|
622
|
+
const ruleFilePathValue = ruleFilePath;
|
|
623
|
+
const xpathValue = xpath;
|
|
624
|
+
const hasRuleFilePath = ruleFilePathValue !== void 0 && ruleFilePathValue.length > MIN_COUNT;
|
|
625
|
+
const hasXpathValue = xpathValue.length > MIN_COUNT;
|
|
626
|
+
const attributeOptions = hasRuleFilePath && hasXpathValue ? {
|
|
627
|
+
lineNumberCollector: (lineNumber) => {
|
|
628
|
+
coveredLineNumbers.add(lineNumber);
|
|
629
|
+
},
|
|
630
|
+
ruleFilePath: ruleFilePathValue,
|
|
631
|
+
xpath: xpathValue
|
|
632
|
+
} : void 0;
|
|
633
|
+
const attributeEvidence = checkAttributeCoverage(
|
|
634
|
+
analysis.attributes,
|
|
635
|
+
allContent,
|
|
636
|
+
attributeOptions
|
|
637
|
+
);
|
|
638
|
+
const attributeSuccess = attributeEvidence.count >= attributeEvidence.required;
|
|
639
|
+
coverageResults.push({
|
|
640
|
+
details: [],
|
|
641
|
+
evidence: [attributeEvidence],
|
|
642
|
+
message: `Attributes: ${String(attributeEvidence.count)}/${String(analysis.attributes.length)} covered`,
|
|
643
|
+
success: attributeSuccess
|
|
644
|
+
});
|
|
645
|
+
if (!attributeSuccess) {
|
|
646
|
+
uncoveredBranches.push(
|
|
647
|
+
`Attributes: ${analysis.attributes.join(", ")}`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (analysis.operators.length > MIN_COUNT) {
|
|
652
|
+
const operatorEvidence = checkOperatorCoverage(
|
|
653
|
+
analysis.operators,
|
|
654
|
+
allContent
|
|
655
|
+
);
|
|
656
|
+
const operatorSuccess = operatorEvidence.count >= operatorEvidence.required;
|
|
657
|
+
coverageResults.push({
|
|
658
|
+
details: [],
|
|
659
|
+
evidence: [operatorEvidence],
|
|
660
|
+
message: `Operators: ${String(operatorEvidence.count)}/${String(analysis.operators.length)} covered`,
|
|
661
|
+
success: operatorSuccess
|
|
662
|
+
});
|
|
663
|
+
if (!operatorSuccess) {
|
|
664
|
+
uncoveredBranches.push(
|
|
665
|
+
`Operators: ${analysis.operators.join(", ")}`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const overallSuccess = coverageResults.length === MIN_COUNT || coverageResults.every(
|
|
670
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameter for every
|
|
671
|
+
(result) => result.success
|
|
672
|
+
);
|
|
673
|
+
return {
|
|
674
|
+
coverage: coverageResults,
|
|
675
|
+
coveredLineNumbers: Array.from(coveredLineNumbers),
|
|
676
|
+
overallSuccess,
|
|
677
|
+
uncoveredBranches
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/parser/extractMarkers.ts
|
|
682
|
+
var LINE_NUMBER_OFFSET = 1;
|
|
683
|
+
function extractMarkers(exampleContent) {
|
|
684
|
+
const lines = exampleContent.split("\n");
|
|
685
|
+
const violationMarkers = [];
|
|
686
|
+
const validMarkers = [];
|
|
687
|
+
const hasInlineMarkersInExample = exampleContent.includes("// \u274C") || exampleContent.includes("// \u2705");
|
|
688
|
+
lines.forEach((line, index) => {
|
|
689
|
+
const trimmed = line.trim();
|
|
690
|
+
const lineNumber = index + LINE_NUMBER_OFFSET;
|
|
691
|
+
if (trimmed.includes("// \u274C")) {
|
|
692
|
+
violationMarkers.push({
|
|
693
|
+
description: "Inline violation marker // \u274C",
|
|
694
|
+
index: violationMarkers.length,
|
|
695
|
+
isViolation: true,
|
|
696
|
+
lineNumber
|
|
697
|
+
});
|
|
698
|
+
} else if (trimmed.includes("// \u2705")) {
|
|
699
|
+
validMarkers.push({
|
|
700
|
+
description: "Inline valid marker // \u2705",
|
|
701
|
+
index: validMarkers.length,
|
|
702
|
+
isViolation: false,
|
|
703
|
+
lineNumber
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
if (!hasInlineMarkersInExample) {
|
|
707
|
+
if (trimmed.startsWith("// Violation:")) {
|
|
708
|
+
const description = trimmed.substring("// Violation:".length).trim();
|
|
709
|
+
violationMarkers.push({
|
|
710
|
+
description,
|
|
711
|
+
index: violationMarkers.length,
|
|
712
|
+
isViolation: true,
|
|
713
|
+
lineNumber
|
|
714
|
+
});
|
|
715
|
+
} else if (trimmed.startsWith("// Valid:")) {
|
|
716
|
+
const description = trimmed.substring("// Valid:".length).trim();
|
|
717
|
+
validMarkers.push({
|
|
718
|
+
description,
|
|
719
|
+
index: validMarkers.length,
|
|
720
|
+
isViolation: false,
|
|
721
|
+
lineNumber
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
return { validMarkers, violationMarkers };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/parser/parseExample.ts
|
|
730
|
+
var EMPTY_STRING_INDEX = 0;
|
|
731
|
+
function parseExample(exampleContent) {
|
|
732
|
+
const lines = exampleContent.split("\n");
|
|
733
|
+
const violations = [];
|
|
734
|
+
const valids = [];
|
|
735
|
+
let currentMode = null;
|
|
736
|
+
const { validMarkers, violationMarkers } = extractMarkers(exampleContent);
|
|
737
|
+
const hasInlineMarkersInExample = exampleContent.includes("// \u274C") || exampleContent.includes("// \u2705");
|
|
738
|
+
lines.forEach((line) => {
|
|
739
|
+
const trimmed = line.trim();
|
|
740
|
+
let lineMode = currentMode;
|
|
741
|
+
if (trimmed.includes("// \u274C")) {
|
|
742
|
+
lineMode = "violation";
|
|
743
|
+
} else if (trimmed.includes("// \u2705")) {
|
|
744
|
+
lineMode = "valid";
|
|
745
|
+
}
|
|
746
|
+
if (trimmed.startsWith("// Violation:")) {
|
|
747
|
+
currentMode = "violation";
|
|
748
|
+
if (!hasInlineMarkersInExample) {
|
|
749
|
+
}
|
|
750
|
+
} else if (trimmed.startsWith("// Valid:")) {
|
|
751
|
+
currentMode = "valid";
|
|
752
|
+
if (!hasInlineMarkersInExample) {
|
|
753
|
+
}
|
|
754
|
+
} else if (trimmed && !trimmed.startsWith("//") && trimmed !== "") {
|
|
755
|
+
let codeLine = line;
|
|
756
|
+
if (line.includes("// \u274C")) {
|
|
757
|
+
const splitResult = line.split("// \u274C");
|
|
758
|
+
codeLine = splitResult[EMPTY_STRING_INDEX].trim();
|
|
759
|
+
} else if (line.includes("// \u2705")) {
|
|
760
|
+
const splitResult = line.split("// \u2705");
|
|
761
|
+
codeLine = splitResult[EMPTY_STRING_INDEX].trim();
|
|
762
|
+
}
|
|
763
|
+
if (lineMode === "violation") {
|
|
764
|
+
violations.push(codeLine);
|
|
765
|
+
} else if (lineMode === "valid") {
|
|
766
|
+
valids.push(codeLine);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
return {
|
|
771
|
+
content: exampleContent,
|
|
772
|
+
validMarkers,
|
|
773
|
+
valids,
|
|
774
|
+
violationMarkers,
|
|
775
|
+
violations
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/parser/createTestFile.ts
|
|
780
|
+
import { writeFileSync } from "fs";
|
|
781
|
+
import tmp from "tmp";
|
|
782
|
+
var EMPTY_LENGTH = 0;
|
|
783
|
+
var FIRST_CAPTURE_GROUP_INDEX = 1;
|
|
784
|
+
var SECOND_CAPTURE_GROUP_INDEX = 2;
|
|
785
|
+
function inferReturnType(code, methodName) {
|
|
786
|
+
if (new RegExp(`\\b${methodName}\\s*\\(\\s*\\)\\s*[><=!]+\\s*\\d+`).test(
|
|
787
|
+
code
|
|
788
|
+
)) {
|
|
789
|
+
return "Integer";
|
|
790
|
+
}
|
|
791
|
+
if (new RegExp(`switch\\s+on\\s+${methodName}\\s*\\(`).test(code)) {
|
|
792
|
+
return "String";
|
|
793
|
+
}
|
|
794
|
+
if (new RegExp(`${methodName}\\s*\\(\\s*\\)\\s*\\?`).test(code)) {
|
|
795
|
+
return "Boolean";
|
|
796
|
+
}
|
|
797
|
+
if (new RegExp(`for\\s*\\([^:]*:\\s*${methodName}\\s*\\(`).test(code)) {
|
|
798
|
+
return "List<String>";
|
|
799
|
+
}
|
|
800
|
+
if (new RegExp(`Set<[^>]+>\\s+\\w+\\s*=\\s*${methodName}\\s*\\(`).test(code)) {
|
|
801
|
+
return "Set<String>";
|
|
802
|
+
}
|
|
803
|
+
if (new RegExp(
|
|
804
|
+
`Map<[^>]+,\\s*[^>]+>\\s+\\w+\\s*=\\s*${methodName}\\s*\\(`
|
|
805
|
+
).test(code)) {
|
|
806
|
+
return "Map<String, Integer>";
|
|
807
|
+
}
|
|
808
|
+
if (new RegExp(`Decimal\\s+\\w+\\s*=\\s*${methodName}\\s*\\(`).test(code)) {
|
|
809
|
+
return "Decimal";
|
|
810
|
+
}
|
|
811
|
+
if (new RegExp(`Double\\s+\\w+\\s*=\\s*${methodName}\\s*\\(`).test(code)) {
|
|
812
|
+
return "Double";
|
|
813
|
+
}
|
|
814
|
+
if (new RegExp(`Long\\s+\\w+\\s*=\\s*${methodName}\\s*\\(`).test(code)) {
|
|
815
|
+
return "Long";
|
|
816
|
+
}
|
|
817
|
+
if (new RegExp(`Date\\s+\\w+\\s*=\\s*${methodName}\\s*\\(`).test(code)) {
|
|
818
|
+
return "Date";
|
|
819
|
+
}
|
|
820
|
+
if (new RegExp(`Datetime\\s+\\w+\\s*=\\s*${methodName}\\s*\\(`).test(code)) {
|
|
821
|
+
return "Datetime";
|
|
822
|
+
}
|
|
823
|
+
if (new RegExp(`Time\\s+\\w+\\s*=\\s*${methodName}\\s*\\(`).test(code)) {
|
|
824
|
+
return "Time";
|
|
825
|
+
}
|
|
826
|
+
if (new RegExp(`Blob\\s+\\w+\\s*=\\s*${methodName}\\s*\\(`).test(code)) {
|
|
827
|
+
return "Blob";
|
|
828
|
+
}
|
|
829
|
+
if (new RegExp(`Id\\s+\\w+\\s*=\\s*${methodName}\\s*\\(`).test(code)) {
|
|
830
|
+
return "Id";
|
|
831
|
+
}
|
|
832
|
+
if (new RegExp(`(?:while|do)\\s*\\(\\s*${methodName}\\s*\\(`).test(code)) {
|
|
833
|
+
return "Boolean";
|
|
834
|
+
}
|
|
835
|
+
if (new RegExp(`Object\\s+\\w+\\s*=\\s*${methodName}\\s*\\(`).test(code)) {
|
|
836
|
+
return "Object";
|
|
837
|
+
}
|
|
838
|
+
return "Boolean";
|
|
839
|
+
}
|
|
840
|
+
function generateReturnValue(returnType) {
|
|
841
|
+
const listRegex = /^List<(.+)>$/;
|
|
842
|
+
const listMatch = listRegex.exec(returnType);
|
|
843
|
+
const listInnerType = listMatch?.[FIRST_CAPTURE_GROUP_INDEX];
|
|
844
|
+
if (listInnerType !== void 0 && listInnerType.length > EMPTY_LENGTH) {
|
|
845
|
+
return `return new List<${listInnerType}>();`;
|
|
846
|
+
}
|
|
847
|
+
const setRegex = /^Set<(.+)>$/;
|
|
848
|
+
const setMatch = setRegex.exec(returnType);
|
|
849
|
+
const setInnerType = setMatch?.[FIRST_CAPTURE_GROUP_INDEX];
|
|
850
|
+
if (setInnerType !== void 0 && setInnerType.length > EMPTY_LENGTH) {
|
|
851
|
+
return `return new Set<${setInnerType}>();`;
|
|
852
|
+
}
|
|
853
|
+
const mapRegex = /^Map<(.+),\s*(.+)>$/;
|
|
854
|
+
const mapMatch = mapRegex.exec(returnType);
|
|
855
|
+
const mapKeyType = mapMatch?.[FIRST_CAPTURE_GROUP_INDEX];
|
|
856
|
+
const mapValueType = mapMatch?.[SECOND_CAPTURE_GROUP_INDEX];
|
|
857
|
+
if (mapKeyType !== void 0 && mapValueType !== void 0 && mapKeyType.length > EMPTY_LENGTH && mapValueType.length > EMPTY_LENGTH) {
|
|
858
|
+
return `return new Map<${mapKeyType}, ${mapValueType}>();`;
|
|
859
|
+
}
|
|
860
|
+
switch (returnType) {
|
|
861
|
+
case "Integer":
|
|
862
|
+
return "return 1;";
|
|
863
|
+
case "String":
|
|
864
|
+
return "return 'test';";
|
|
865
|
+
case "Boolean":
|
|
866
|
+
return "return true;";
|
|
867
|
+
case "Decimal":
|
|
868
|
+
return "return 1.0;";
|
|
869
|
+
case "Double":
|
|
870
|
+
return "return 1.0;";
|
|
871
|
+
case "Long":
|
|
872
|
+
return "return 1000L;";
|
|
873
|
+
case "Date":
|
|
874
|
+
return "return Date.newInstance(2024, 1, 1);";
|
|
875
|
+
case "Datetime":
|
|
876
|
+
return "return Datetime.newInstance(2024, 1, 1);";
|
|
877
|
+
case "Time":
|
|
878
|
+
return "return Time.newInstance(0, 0, 0, 0);";
|
|
879
|
+
case "Blob":
|
|
880
|
+
return "return Blob.valueOf('test');";
|
|
881
|
+
case "Id":
|
|
882
|
+
return "return '001000000000000000';";
|
|
883
|
+
case "Object":
|
|
884
|
+
return "return null;";
|
|
885
|
+
// All types from inferReturnType are handled above
|
|
886
|
+
// TypeScript ensures exhaustiveness, so no default needed
|
|
887
|
+
// However, we keep a default for runtime safety in case inferReturnType is extended
|
|
888
|
+
default:
|
|
889
|
+
return "return null;";
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function extractHelperMethods(codeLines) {
|
|
893
|
+
const methods = /* @__PURE__ */ new Map();
|
|
894
|
+
const fullCode = codeLines.join("\n");
|
|
895
|
+
const methodCallRegex = /\b([a-zA-Z_]\w*)\s*\(/g;
|
|
896
|
+
let match = null;
|
|
897
|
+
const foundMethods = /* @__PURE__ */ new Set();
|
|
898
|
+
const apexKeywords = [
|
|
899
|
+
"if",
|
|
900
|
+
"else",
|
|
901
|
+
"for",
|
|
902
|
+
"while",
|
|
903
|
+
"do",
|
|
904
|
+
"switch",
|
|
905
|
+
"case",
|
|
906
|
+
"default",
|
|
907
|
+
"try",
|
|
908
|
+
"catch",
|
|
909
|
+
"finally",
|
|
910
|
+
"class",
|
|
911
|
+
"interface",
|
|
912
|
+
"enum",
|
|
913
|
+
"public",
|
|
914
|
+
"private",
|
|
915
|
+
"protected",
|
|
916
|
+
"static",
|
|
917
|
+
"final",
|
|
918
|
+
"abstract",
|
|
919
|
+
"void",
|
|
920
|
+
"return",
|
|
921
|
+
"new",
|
|
922
|
+
"this",
|
|
923
|
+
"super",
|
|
924
|
+
"extends",
|
|
925
|
+
"implements",
|
|
926
|
+
"instanceof",
|
|
927
|
+
"System",
|
|
928
|
+
"debug",
|
|
929
|
+
"List",
|
|
930
|
+
"Map",
|
|
931
|
+
"Set",
|
|
932
|
+
"String",
|
|
933
|
+
"Integer",
|
|
934
|
+
"Boolean",
|
|
935
|
+
"Double",
|
|
936
|
+
"Date",
|
|
937
|
+
"Datetime"
|
|
938
|
+
];
|
|
939
|
+
while ((match = methodCallRegex.exec(fullCode)) !== null) {
|
|
940
|
+
const [, methodName] = match;
|
|
941
|
+
if (methodName !== void 0 && apexKeywords.includes(methodName)) {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
foundMethods.add(methodName);
|
|
945
|
+
}
|
|
946
|
+
for (const methodName of foundMethods) {
|
|
947
|
+
const returnType = inferReturnType(fullCode, methodName);
|
|
948
|
+
const validReturnType = returnType;
|
|
949
|
+
const methodSignature = `public ${validReturnType} ${methodName}() {
|
|
950
|
+
${generateReturnValue(validReturnType)}
|
|
951
|
+
}`;
|
|
952
|
+
methods.set(methodName, methodSignature);
|
|
953
|
+
}
|
|
954
|
+
return Array.from(methods.values());
|
|
955
|
+
}
|
|
956
|
+
function createTestFile({
|
|
957
|
+
exampleContent,
|
|
958
|
+
exampleIndex,
|
|
959
|
+
includeViolations = true,
|
|
960
|
+
includeValids = true
|
|
961
|
+
}) {
|
|
962
|
+
const tmpFile = tmp.fileSync({
|
|
963
|
+
keep: true,
|
|
964
|
+
postfix: ".cls",
|
|
965
|
+
prefix: `rule-test-example-${String(exampleIndex)}-`
|
|
966
|
+
});
|
|
967
|
+
const tempFile = tmpFile.name;
|
|
968
|
+
const parsed = parseExample(exampleContent);
|
|
969
|
+
let classContent = `public class TestClass${String(exampleIndex)} {
|
|
970
|
+
`;
|
|
971
|
+
let codeToInclude = [];
|
|
972
|
+
if (includeViolations && !includeValids) {
|
|
973
|
+
codeToInclude = parsed.violations;
|
|
974
|
+
} else if (includeValids && !includeViolations) {
|
|
975
|
+
codeToInclude = parsed.valids;
|
|
976
|
+
} else {
|
|
977
|
+
codeToInclude = [...parsed.violations, ...parsed.valids];
|
|
978
|
+
}
|
|
979
|
+
if (codeToInclude.length > EMPTY_LENGTH) {
|
|
980
|
+
classContent += ` public void testMethod${String(exampleIndex)}() {
|
|
981
|
+
`;
|
|
982
|
+
codeToInclude.forEach((line) => {
|
|
983
|
+
classContent += ` ${line}
|
|
984
|
+
`;
|
|
985
|
+
});
|
|
986
|
+
classContent += ` }
|
|
987
|
+
`;
|
|
988
|
+
}
|
|
989
|
+
const helperMethods = extractHelperMethods(codeToInclude);
|
|
990
|
+
for (const method of helperMethods) {
|
|
991
|
+
classContent += ` ${method}
|
|
992
|
+
`;
|
|
993
|
+
}
|
|
994
|
+
classContent += "}\n";
|
|
995
|
+
writeFileSync(tempFile, classContent, "utf-8");
|
|
996
|
+
return {
|
|
997
|
+
filePath: tempFile,
|
|
998
|
+
hasValids: parsed.valids.length > EMPTY_LENGTH,
|
|
999
|
+
hasViolations: parsed.violations.length > EMPTY_LENGTH,
|
|
1000
|
+
validCount: parsed.valids.length,
|
|
1001
|
+
violationCount: parsed.violations.length
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// src/pmd/runPMD.ts
|
|
1006
|
+
import { execFileSync } from "child_process";
|
|
1007
|
+
import { resolve as resolve2 } from "path";
|
|
1008
|
+
|
|
1009
|
+
// src/pmd/parseViolations.ts
|
|
1010
|
+
import { DOMParser as DOMParser2 } from "@xmldom/xmldom";
|
|
1011
|
+
var DEFAULT_LINE = 0;
|
|
1012
|
+
var DEFAULT_COLUMN = 0;
|
|
1013
|
+
var DEFAULT_PRIORITY = "5";
|
|
1014
|
+
var DEFAULT_MESSAGE = "";
|
|
1015
|
+
var RADIX = 10;
|
|
1016
|
+
var MIN_STRING_LENGTH4 = 0;
|
|
1017
|
+
var MIN_TEXT_LENGTH = 0;
|
|
1018
|
+
function parseViolations(xmlOutput) {
|
|
1019
|
+
const parser = new DOMParser2();
|
|
1020
|
+
const doc = parser.parseFromString(xmlOutput, "text/xml");
|
|
1021
|
+
const violations = [];
|
|
1022
|
+
const fileNodes = Array.from(doc.getElementsByTagName("file"));
|
|
1023
|
+
for (const fileNode of fileNodes) {
|
|
1024
|
+
const violationNodes = Array.from(
|
|
1025
|
+
fileNode.getElementsByTagName("violation")
|
|
1026
|
+
);
|
|
1027
|
+
for (const violationNode of violationNodes) {
|
|
1028
|
+
const beginlineAttr = violationNode.getAttribute("beginline");
|
|
1029
|
+
const begincolumnAttr = violationNode.getAttribute("begincolumn");
|
|
1030
|
+
const priorityAttr = violationNode.getAttribute("priority");
|
|
1031
|
+
const ruleAttr = violationNode.getAttribute("rule");
|
|
1032
|
+
const messageAttr = violationNode.getAttribute("message");
|
|
1033
|
+
const rawTextContent = violationNode.textContent;
|
|
1034
|
+
const trimmedText = rawTextContent.trim();
|
|
1035
|
+
const hasTextContent = trimmedText.length > MIN_TEXT_LENGTH;
|
|
1036
|
+
const textContent = hasTextContent ? trimmedText : DEFAULT_MESSAGE;
|
|
1037
|
+
const hasBeginline = beginlineAttr !== null && beginlineAttr.length > MIN_STRING_LENGTH4;
|
|
1038
|
+
const line = parseInt(
|
|
1039
|
+
hasBeginline ? beginlineAttr : String(DEFAULT_LINE),
|
|
1040
|
+
RADIX
|
|
1041
|
+
);
|
|
1042
|
+
const hasBegincolumn = begincolumnAttr !== null && begincolumnAttr.length > MIN_STRING_LENGTH4;
|
|
1043
|
+
const column = parseInt(
|
|
1044
|
+
hasBegincolumn ? begincolumnAttr : String(DEFAULT_COLUMN),
|
|
1045
|
+
RADIX
|
|
1046
|
+
);
|
|
1047
|
+
const hasMessageAttr = messageAttr !== null && messageAttr.length > MIN_STRING_LENGTH4;
|
|
1048
|
+
const message = hasMessageAttr ? messageAttr : textContent;
|
|
1049
|
+
const hasPriorityAttr = priorityAttr !== null && priorityAttr.length > MIN_STRING_LENGTH4;
|
|
1050
|
+
const priority = parseInt(
|
|
1051
|
+
hasPriorityAttr ? priorityAttr : DEFAULT_PRIORITY,
|
|
1052
|
+
RADIX
|
|
1053
|
+
);
|
|
1054
|
+
const hasRuleAttr = ruleAttr !== null && ruleAttr.length > MIN_STRING_LENGTH4;
|
|
1055
|
+
const rule = hasRuleAttr ? ruleAttr : DEFAULT_MESSAGE;
|
|
1056
|
+
const violation = {
|
|
1057
|
+
column,
|
|
1058
|
+
line,
|
|
1059
|
+
message,
|
|
1060
|
+
priority,
|
|
1061
|
+
rule
|
|
1062
|
+
};
|
|
1063
|
+
violations.push(violation);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return violations;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// src/pmd/runPMD.ts
|
|
1070
|
+
var MIN_OUTPUT_LENGTH = 0;
|
|
1071
|
+
var EMPTY_STRING = "";
|
|
1072
|
+
async function runPMD(apexFilePath, rulesetPath) {
|
|
1073
|
+
try {
|
|
1074
|
+
const absoluteApexPath = resolve2(apexFilePath);
|
|
1075
|
+
const absoluteRulesetPath = resolve2(rulesetPath);
|
|
1076
|
+
const result = execFileSync(
|
|
1077
|
+
"pmd",
|
|
1078
|
+
[
|
|
1079
|
+
"check",
|
|
1080
|
+
"--no-cache",
|
|
1081
|
+
"--no-progress",
|
|
1082
|
+
"-d",
|
|
1083
|
+
absoluteApexPath,
|
|
1084
|
+
"-R",
|
|
1085
|
+
absoluteRulesetPath,
|
|
1086
|
+
"-f",
|
|
1087
|
+
"xml"
|
|
1088
|
+
],
|
|
1089
|
+
{
|
|
1090
|
+
cwd: process.cwd(),
|
|
1091
|
+
encoding: "utf-8",
|
|
1092
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1093
|
+
timeout: 3e4
|
|
1094
|
+
}
|
|
1095
|
+
);
|
|
1096
|
+
const violations = parseViolations(result);
|
|
1097
|
+
return {
|
|
1098
|
+
data: {
|
|
1099
|
+
violations
|
|
1100
|
+
},
|
|
1101
|
+
success: true
|
|
1102
|
+
};
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
const execError = error;
|
|
1105
|
+
if (execError.code === "ENOENT") {
|
|
1106
|
+
return {
|
|
1107
|
+
error: "PMD CLI not available. Please install PMD to run tests. Visit: https://pmd.github.io/pmd/pmd_userdocs_installation.html",
|
|
1108
|
+
success: false
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
const xmlOutput = execError.stdout ?? EMPTY_STRING;
|
|
1112
|
+
const xmlOutputString = typeof xmlOutput === "string" ? xmlOutput : xmlOutput.toString();
|
|
1113
|
+
if (xmlOutputString.trim().length > MIN_OUTPUT_LENGTH) {
|
|
1114
|
+
try {
|
|
1115
|
+
const violations = parseViolations(xmlOutputString);
|
|
1116
|
+
return {
|
|
1117
|
+
data: {
|
|
1118
|
+
violations
|
|
1119
|
+
},
|
|
1120
|
+
success: true
|
|
1121
|
+
};
|
|
1122
|
+
} catch {
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
const errorMessage = execError.message ?? "Unknown error";
|
|
1126
|
+
let fullErrorMessage = `PMD execution failed: ${errorMessage}`;
|
|
1127
|
+
if (execError.stderr !== void 0) {
|
|
1128
|
+
const stderr = typeof execError.stderr === "string" ? execError.stderr : execError.stderr.toString();
|
|
1129
|
+
if (stderr.trim().length > MIN_OUTPUT_LENGTH) {
|
|
1130
|
+
fullErrorMessage += `
|
|
1131
|
+
PMD stderr:
|
|
1132
|
+
${stderr}`;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (execError.stdout !== void 0) {
|
|
1136
|
+
const stdout = typeof execError.stdout === "string" ? execError.stdout : execError.stdout.toString();
|
|
1137
|
+
if (stdout.length > MIN_OUTPUT_LENGTH) {
|
|
1138
|
+
fullErrorMessage += `
|
|
1139
|
+
PMD stdout:
|
|
1140
|
+
${stdout}`;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
error: fullErrorMessage,
|
|
1145
|
+
success: false
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// src/tester/quality/checkRuleMetadata.ts
|
|
1151
|
+
var MIN_RULE_NAME_LENGTH = 3;
|
|
1152
|
+
var MIN_MESSAGE_LENGTH = 10;
|
|
1153
|
+
var MIN_DESCRIPTION_LENGTH = 20;
|
|
1154
|
+
var MIN_HARDCODED_STRING_LENGTH = 4;
|
|
1155
|
+
var MIN_ERROR_COUNT = 0;
|
|
1156
|
+
var MIN_MATCH_COUNT = 0;
|
|
1157
|
+
function containsHardcodedValues(xpath) {
|
|
1158
|
+
const hardcodedNumbers = xpath.match(/\b[2-9]\d*\b/g);
|
|
1159
|
+
if (hardcodedNumbers !== null && hardcodedNumbers.length > MIN_MATCH_COUNT) {
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
const hardcodedStrings = xpath.match(/["'][^"']*["']/g);
|
|
1163
|
+
const hasLongHardcodedStrings = hardcodedStrings?.some(
|
|
1164
|
+
(str) => str.length > MIN_HARDCODED_STRING_LENGTH
|
|
1165
|
+
);
|
|
1166
|
+
if (hasLongHardcodedStrings === true) {
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
function checkRuleMetadata(metadata) {
|
|
1172
|
+
const errors = [];
|
|
1173
|
+
const warnings = [];
|
|
1174
|
+
if (metadata.ruleName === null || metadata.ruleName === void 0 || metadata.ruleName === "") {
|
|
1175
|
+
errors.push("Rule name is missing");
|
|
1176
|
+
}
|
|
1177
|
+
if (metadata.message === null || metadata.message === void 0 || metadata.message === "") {
|
|
1178
|
+
errors.push("Rule message is missing");
|
|
1179
|
+
}
|
|
1180
|
+
if (metadata.description === null || metadata.description === void 0 || metadata.description === "") {
|
|
1181
|
+
warnings.push("Rule description is missing (recommended)");
|
|
1182
|
+
}
|
|
1183
|
+
if (metadata.xpath === null || metadata.xpath === void 0 || metadata.xpath === "") {
|
|
1184
|
+
errors.push("Rule XPath expression is missing");
|
|
1185
|
+
}
|
|
1186
|
+
if (metadata.ruleName !== null && metadata.ruleName !== void 0 && metadata.ruleName.length < MIN_RULE_NAME_LENGTH) {
|
|
1187
|
+
const minLengthStr = String(MIN_RULE_NAME_LENGTH);
|
|
1188
|
+
warnings.push(
|
|
1189
|
+
`Rule name is very short (less than ${minLengthStr} characters)`
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
if (metadata.message !== null && metadata.message !== void 0 && metadata.message.length < MIN_MESSAGE_LENGTH) {
|
|
1193
|
+
const minLengthStr = String(MIN_MESSAGE_LENGTH);
|
|
1194
|
+
warnings.push(
|
|
1195
|
+
`Rule message is very short (less than ${minLengthStr} characters)`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
if (metadata.description !== null && metadata.description !== void 0 && metadata.description.length < MIN_DESCRIPTION_LENGTH) {
|
|
1199
|
+
const minLengthStr = String(MIN_DESCRIPTION_LENGTH);
|
|
1200
|
+
warnings.push(
|
|
1201
|
+
`Rule description is very short (less than ${minLengthStr} characters)`
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
const hasXPath = metadata.xpath !== null && metadata.xpath !== void 0 && metadata.xpath !== "";
|
|
1205
|
+
if (hasXPath && containsHardcodedValues(metadata.xpath)) {
|
|
1206
|
+
warnings.push(
|
|
1207
|
+
"XPath contains hardcoded values that should be parameterized"
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
return {
|
|
1211
|
+
issues: errors,
|
|
1212
|
+
passed: errors.length === MIN_ERROR_COUNT,
|
|
1213
|
+
warnings
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/tester/quality/checkExamples.ts
|
|
1218
|
+
var MIN_EXAMPLES_COUNT = 0;
|
|
1219
|
+
var MIN_VIOLATION_MARKERS = 0;
|
|
1220
|
+
var MIN_VALID_MARKERS = 0;
|
|
1221
|
+
var MIN_VIOLATIONS = 0;
|
|
1222
|
+
var MIN_VALIDS = 0;
|
|
1223
|
+
var MIN_MARKERS = 0;
|
|
1224
|
+
var MIN_CODE_LINES = 0;
|
|
1225
|
+
var INDEX_OFFSET = 1;
|
|
1226
|
+
function checkMarkerConsistency(example, exampleNum, warnings) {
|
|
1227
|
+
const totalMarkers = example.violationMarkers.length + example.validMarkers.length;
|
|
1228
|
+
const totalCodeLines = example.violations.length + example.valids.length;
|
|
1229
|
+
if (totalMarkers === MIN_MARKERS && totalCodeLines > MIN_CODE_LINES) {
|
|
1230
|
+
const exampleNumStr = String(exampleNum);
|
|
1231
|
+
warnings.push(`Example ${exampleNumStr} has code but no markers`);
|
|
1232
|
+
}
|
|
1233
|
+
if (totalMarkers > MIN_MARKERS && totalCodeLines === MIN_CODE_LINES) {
|
|
1234
|
+
const exampleNumStr = String(exampleNum);
|
|
1235
|
+
warnings.push(`Example ${exampleNumStr} has markers but no code`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function checkExamples(examples) {
|
|
1239
|
+
const errors = [];
|
|
1240
|
+
const warnings = [];
|
|
1241
|
+
if (examples.length === MIN_EXAMPLES_COUNT) {
|
|
1242
|
+
errors.push("No examples found in rule");
|
|
1243
|
+
return { issues: errors, passed: false, warnings };
|
|
1244
|
+
}
|
|
1245
|
+
examples.forEach(
|
|
1246
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameters for forEach
|
|
1247
|
+
(example, index) => {
|
|
1248
|
+
const exampleNum = index + INDEX_OFFSET;
|
|
1249
|
+
if (example.violationMarkers.length === MIN_VIOLATION_MARKERS) {
|
|
1250
|
+
const exampleNumStr = String(exampleNum);
|
|
1251
|
+
warnings.push(
|
|
1252
|
+
`Example ${exampleNumStr} has no violation markers`
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
if (example.validMarkers.length === MIN_VALID_MARKERS) {
|
|
1256
|
+
const exampleNumStr = String(exampleNum);
|
|
1257
|
+
warnings.push(`Example ${exampleNumStr} has no valid markers`);
|
|
1258
|
+
}
|
|
1259
|
+
if (example.violations.length === MIN_VIOLATIONS && example.valids.length === MIN_VALIDS) {
|
|
1260
|
+
const exampleNumStr = String(exampleNum);
|
|
1261
|
+
errors.push(`Example ${exampleNumStr} contains no code`);
|
|
1262
|
+
}
|
|
1263
|
+
const hasViolations = example.violations.length > MIN_VIOLATIONS;
|
|
1264
|
+
const hasValids = example.valids.length > MIN_VALIDS;
|
|
1265
|
+
if (hasViolations && hasValids && example.violationMarkers.length > MIN_VIOLATION_MARKERS && example.validMarkers.length > MIN_VALID_MARKERS) {
|
|
1266
|
+
} else if (hasViolations && !hasValids) {
|
|
1267
|
+
if (example.violationMarkers.length === MIN_VIOLATION_MARKERS) {
|
|
1268
|
+
const exampleNumStr = String(exampleNum);
|
|
1269
|
+
warnings.push(
|
|
1270
|
+
`Example ${exampleNumStr} has violations but no violation markers`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
} else if (!hasViolations && hasValids) {
|
|
1274
|
+
if (example.validMarkers.length === MIN_VALID_MARKERS) {
|
|
1275
|
+
const exampleNumStr = String(exampleNum);
|
|
1276
|
+
warnings.push(
|
|
1277
|
+
`Example ${exampleNumStr} has valid code but no valid markers`
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
checkMarkerConsistency(example, exampleNum, warnings);
|
|
1282
|
+
}
|
|
1283
|
+
);
|
|
1284
|
+
const noErrors = errors.length === MIN_EXAMPLES_COUNT;
|
|
1285
|
+
return {
|
|
1286
|
+
issues: errors,
|
|
1287
|
+
passed: noErrors,
|
|
1288
|
+
warnings
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// src/tester/quality/checkDuplicates.ts
|
|
1293
|
+
var MIN_EXAMPLES_FOR_DUPLICATE_CHECK = 2;
|
|
1294
|
+
var MIN_DUPLICATE_COUNT = 1;
|
|
1295
|
+
var MIN_PATTERN_LENGTH = 10;
|
|
1296
|
+
var PATTERN_DISPLAY_LENGTH = 50;
|
|
1297
|
+
var INDEX_OFFSET2 = 1;
|
|
1298
|
+
var ZERO_ERRORS = 0;
|
|
1299
|
+
var MIN_COUNT2 = 0;
|
|
1300
|
+
function normalizeCode(code) {
|
|
1301
|
+
return code.replace(/\s+/g, " ").trim().toLowerCase();
|
|
1302
|
+
}
|
|
1303
|
+
function checkPatternDuplicates(patterns, type, warnings) {
|
|
1304
|
+
for (const [pattern, exampleNumbers] of patterns.entries()) {
|
|
1305
|
+
if (exampleNumbers.length > MIN_DUPLICATE_COUNT && pattern.length > MIN_PATTERN_LENGTH) {
|
|
1306
|
+
const patternPreview = pattern.substring(
|
|
1307
|
+
MIN_COUNT2,
|
|
1308
|
+
PATTERN_DISPLAY_LENGTH
|
|
1309
|
+
);
|
|
1310
|
+
warnings.push(
|
|
1311
|
+
`Duplicate ${type} pattern "${patternPreview}..." found in examples: ${exampleNumbers.join(", ")}`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
function checkDuplicates(examples) {
|
|
1317
|
+
const errors = [];
|
|
1318
|
+
const warnings = [];
|
|
1319
|
+
if (examples.length < MIN_EXAMPLES_FOR_DUPLICATE_CHECK) {
|
|
1320
|
+
return { issues: errors, passed: true, warnings };
|
|
1321
|
+
}
|
|
1322
|
+
const violationPatterns = /* @__PURE__ */ new Map();
|
|
1323
|
+
const validPatterns = /* @__PURE__ */ new Map();
|
|
1324
|
+
examples.forEach(
|
|
1325
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameters for forEach
|
|
1326
|
+
(example, index) => {
|
|
1327
|
+
const exampleNum = index + INDEX_OFFSET2;
|
|
1328
|
+
example.violations.forEach((code) => {
|
|
1329
|
+
const normalized = normalizeCode(code);
|
|
1330
|
+
if (!violationPatterns.has(normalized)) {
|
|
1331
|
+
violationPatterns.set(normalized, []);
|
|
1332
|
+
}
|
|
1333
|
+
const patternList = violationPatterns.get(normalized);
|
|
1334
|
+
patternList.push(exampleNum);
|
|
1335
|
+
});
|
|
1336
|
+
example.valids.forEach((code) => {
|
|
1337
|
+
const normalized = normalizeCode(code);
|
|
1338
|
+
if (!validPatterns.has(normalized)) {
|
|
1339
|
+
validPatterns.set(normalized, []);
|
|
1340
|
+
}
|
|
1341
|
+
const patternList = validPatterns.get(normalized);
|
|
1342
|
+
patternList.push(exampleNum);
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
);
|
|
1346
|
+
checkPatternDuplicates(violationPatterns, "violation", warnings);
|
|
1347
|
+
checkPatternDuplicates(validPatterns, "valid", warnings);
|
|
1348
|
+
const noErrors = errors.length === ZERO_ERRORS;
|
|
1349
|
+
return {
|
|
1350
|
+
issues: errors,
|
|
1351
|
+
passed: noErrors,
|
|
1352
|
+
warnings
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/tester/qualityChecks.ts
|
|
1357
|
+
var MIN_ISSUES_COUNT = 0;
|
|
1358
|
+
function runQualityChecks(ruleMetadata, examples) {
|
|
1359
|
+
const issues = [];
|
|
1360
|
+
const warnings = [];
|
|
1361
|
+
const metadataResult = checkRuleMetadata(ruleMetadata);
|
|
1362
|
+
issues.push(...metadataResult.issues);
|
|
1363
|
+
warnings.push(...metadataResult.warnings);
|
|
1364
|
+
const examplesResult = checkExamples(examples);
|
|
1365
|
+
issues.push(...examplesResult.issues);
|
|
1366
|
+
warnings.push(...examplesResult.warnings);
|
|
1367
|
+
const duplicatesResult = checkDuplicates(examples);
|
|
1368
|
+
issues.push(...duplicatesResult.issues);
|
|
1369
|
+
warnings.push(...duplicatesResult.warnings);
|
|
1370
|
+
return {
|
|
1371
|
+
issues,
|
|
1372
|
+
passed: issues.length === MIN_ISSUES_COUNT,
|
|
1373
|
+
warnings
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// src/tester/RuleTester.ts
|
|
1378
|
+
var MIN_EXAMPLES_COUNT2 = 0;
|
|
1379
|
+
var MIN_VIOLATIONS_COUNT = 0;
|
|
1380
|
+
var EMPTY_STRING2 = "";
|
|
1381
|
+
var DEFAULT_CONCURRENCY = 1;
|
|
1382
|
+
function getAttributeValue(element, name) {
|
|
1383
|
+
return element.getAttribute(name);
|
|
1384
|
+
}
|
|
1385
|
+
var RuleTester = class {
|
|
1386
|
+
/**
|
|
1387
|
+
* Creates a new RuleTester instance.
|
|
1388
|
+
* @param ruleFilePath - Absolute or relative path to the PMD rule XML file.
|
|
1389
|
+
* @throws {Error} If rule file does not exist or cannot be read.
|
|
1390
|
+
*/
|
|
1391
|
+
constructor(ruleFilePath) {
|
|
1392
|
+
if (!existsSync(ruleFilePath)) {
|
|
1393
|
+
throw new Error(`Rule file not found: ${ruleFilePath}`);
|
|
1394
|
+
}
|
|
1395
|
+
if (!ruleFilePath.endsWith(".xml")) {
|
|
1396
|
+
throw new Error("Rule file must have .xml extension");
|
|
1397
|
+
}
|
|
1398
|
+
this.ruleFilePath = ruleFilePath;
|
|
1399
|
+
this.ruleMetadata = this.extractRuleMetadata();
|
|
1400
|
+
this.ruleName = this.ruleMetadata.ruleName ?? "unknown";
|
|
1401
|
+
this.category = this.extractCategory(ruleFilePath);
|
|
1402
|
+
this.examples = [];
|
|
1403
|
+
this.results = this.initializeResults();
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Extract category from rule file path.
|
|
1407
|
+
* @param ruleFilePath - Path to the rule file.
|
|
1408
|
+
* @returns Category name.
|
|
1409
|
+
* @private
|
|
1410
|
+
*/
|
|
1411
|
+
extractCategory(ruleFilePath) {
|
|
1412
|
+
void this.ruleFilePath;
|
|
1413
|
+
const pathParts = ruleFilePath.split("/");
|
|
1414
|
+
const rulesetsIndex = pathParts.findIndex(
|
|
1415
|
+
(part) => part === "rulesets"
|
|
1416
|
+
);
|
|
1417
|
+
const minIndex = -1;
|
|
1418
|
+
const categoryIndexOffset = 1;
|
|
1419
|
+
if (rulesetsIndex !== minIndex && rulesetsIndex < pathParts.length - categoryIndexOffset) {
|
|
1420
|
+
const categoryIndex = rulesetsIndex + categoryIndexOffset;
|
|
1421
|
+
const category = pathParts[categoryIndex];
|
|
1422
|
+
return category;
|
|
1423
|
+
}
|
|
1424
|
+
return "unknown";
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Extracts rule metadata (name, message, description, XPath) from the rule XML file.
|
|
1428
|
+
* @returns Parsed rule metadata object.
|
|
1429
|
+
* @public
|
|
1430
|
+
*/
|
|
1431
|
+
extractRuleMetadata() {
|
|
1432
|
+
const content = readFileSync3(this.ruleFilePath, "utf-8");
|
|
1433
|
+
const parser = new DOMParser3();
|
|
1434
|
+
const doc = parser.parseFromString(content, "text/xml");
|
|
1435
|
+
const ruleElement = doc.getElementsByTagName("rule")[MIN_EXAMPLES_COUNT2];
|
|
1436
|
+
if (!ruleElement) {
|
|
1437
|
+
return {
|
|
1438
|
+
description: null,
|
|
1439
|
+
message: null,
|
|
1440
|
+
ruleName: null,
|
|
1441
|
+
xpath: null
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
const ruleName = getAttributeValue(ruleElement, "name");
|
|
1445
|
+
const message = getAttributeValue(ruleElement, "message");
|
|
1446
|
+
const descriptionElements = ruleElement.getElementsByTagName("description");
|
|
1447
|
+
let description = null;
|
|
1448
|
+
if (descriptionElements.length > MIN_EXAMPLES_COUNT2) {
|
|
1449
|
+
const descElement = descriptionElements[MIN_EXAMPLES_COUNT2];
|
|
1450
|
+
const { textContent } = descElement;
|
|
1451
|
+
if (
|
|
1452
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- textContent can be null at runtime
|
|
1453
|
+
textContent !== null && textContent.trim() !== EMPTY_STRING2
|
|
1454
|
+
) {
|
|
1455
|
+
description = textContent.trim();
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const xpathResult = extractXPath(this.ruleFilePath);
|
|
1459
|
+
let xpath = null;
|
|
1460
|
+
if (xpathResult.success && xpathResult.data !== null && xpathResult.data !== void 0) {
|
|
1461
|
+
xpath = xpathResult.data;
|
|
1462
|
+
}
|
|
1463
|
+
return { description, message, ruleName, xpath };
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Extracts examples from the rule XML file.
|
|
1467
|
+
* @returns Array of parsed example data.
|
|
1468
|
+
* @public
|
|
1469
|
+
*/
|
|
1470
|
+
extractExamples() {
|
|
1471
|
+
const content = readFileSync3(this.ruleFilePath, "utf-8");
|
|
1472
|
+
const parser = new DOMParser3();
|
|
1473
|
+
const doc = parser.parseFromString(content, "text/xml");
|
|
1474
|
+
const exampleNodes = doc.getElementsByTagName("example");
|
|
1475
|
+
const extractedExamples = [];
|
|
1476
|
+
const indexOffset = 1;
|
|
1477
|
+
for (let i = 0; i < exampleNodes.length; i++) {
|
|
1478
|
+
const exampleNode = exampleNodes[i];
|
|
1479
|
+
const { textContent } = exampleNode;
|
|
1480
|
+
const MIN_CONTENT_LENGTH = 0;
|
|
1481
|
+
if (textContent.length === MIN_CONTENT_LENGTH) continue;
|
|
1482
|
+
const exampleContent = textContent.trim();
|
|
1483
|
+
if (exampleContent.length > MIN_CONTENT_LENGTH) {
|
|
1484
|
+
const parsedExample = parseExample(exampleContent);
|
|
1485
|
+
extractedExamples.push({
|
|
1486
|
+
content: exampleContent,
|
|
1487
|
+
exampleIndex: i + indexOffset,
|
|
1488
|
+
validMarkers: parsedExample.validMarkers,
|
|
1489
|
+
valids: parsedExample.valids,
|
|
1490
|
+
violationMarkers: parsedExample.violationMarkers,
|
|
1491
|
+
violations: parsedExample.violations
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
this.examples = extractedExamples;
|
|
1496
|
+
return this.examples;
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Gets the rule metadata.
|
|
1500
|
+
* @returns Rule metadata object.
|
|
1501
|
+
* @public
|
|
1502
|
+
*/
|
|
1503
|
+
getRuleMetadata() {
|
|
1504
|
+
return this.ruleMetadata;
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Gets the extracted examples.
|
|
1508
|
+
* @returns Array of parsed example data.
|
|
1509
|
+
* @public
|
|
1510
|
+
*/
|
|
1511
|
+
getExamples() {
|
|
1512
|
+
return this.examples;
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Cleans up temporary files created during testing.
|
|
1516
|
+
* @public
|
|
1517
|
+
*/
|
|
1518
|
+
cleanup() {
|
|
1519
|
+
void this.ruleFilePath;
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Runs comprehensive rule testing including PMD execution, quality checks, and XPath analysis.
|
|
1523
|
+
* @param skipPMDValidation - Skip actual PMD validation (for testing).
|
|
1524
|
+
* @param maxConcurrency - Maximum number of examples to test concurrently.
|
|
1525
|
+
* @returns Promise resolving to complete test results.
|
|
1526
|
+
* @public
|
|
1527
|
+
*/
|
|
1528
|
+
async runCoverageTest(skipPMDValidation = false, maxConcurrency = DEFAULT_CONCURRENCY) {
|
|
1529
|
+
this.extractExamples();
|
|
1530
|
+
const qualityResult = runQualityChecks(
|
|
1531
|
+
this.ruleMetadata,
|
|
1532
|
+
this.examples
|
|
1533
|
+
);
|
|
1534
|
+
const INDEX_OFFSET3 = 1;
|
|
1535
|
+
const ZERO_VIOLATIONS = 0;
|
|
1536
|
+
const exampleResults = skipPMDValidation ? (
|
|
1537
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameter for map
|
|
1538
|
+
this.examples.map((_example, i) => ({
|
|
1539
|
+
actualViolations: ZERO_VIOLATIONS,
|
|
1540
|
+
exampleIndex: i + INDEX_OFFSET3,
|
|
1541
|
+
expectedValids: ZERO_VIOLATIONS,
|
|
1542
|
+
expectedViolations: ZERO_VIOLATIONS,
|
|
1543
|
+
passed: true,
|
|
1544
|
+
testCaseResults: []
|
|
1545
|
+
}))
|
|
1546
|
+
) : await this.validateExamplesWithPMD(maxConcurrency);
|
|
1547
|
+
this.results.examplesTested = this.examples.length;
|
|
1548
|
+
this.results.examplesPassed = exampleResults.filter(
|
|
1549
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameter
|
|
1550
|
+
(r) => r.passed
|
|
1551
|
+
).length;
|
|
1552
|
+
this.results.totalViolations = exampleResults.reduce(
|
|
1553
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameters
|
|
1554
|
+
(sum, result) => sum + result.actualViolations,
|
|
1555
|
+
MIN_VIOLATIONS_COUNT
|
|
1556
|
+
);
|
|
1557
|
+
this.results.ruleTriggersViolations = exampleResults.some(
|
|
1558
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameter
|
|
1559
|
+
(result) => result.actualViolations > MIN_VIOLATIONS_COUNT
|
|
1560
|
+
);
|
|
1561
|
+
this.results.detailedTestResults = exampleResults.flatMap(
|
|
1562
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameter
|
|
1563
|
+
(result) => result.testCaseResults
|
|
1564
|
+
);
|
|
1565
|
+
const xpathCoverage = checkXPathCoverage(
|
|
1566
|
+
this.ruleMetadata.xpath,
|
|
1567
|
+
this.examples,
|
|
1568
|
+
this.ruleFilePath
|
|
1569
|
+
);
|
|
1570
|
+
this.results.xpathCoverage = xpathCoverage;
|
|
1571
|
+
this.results.success = qualityResult.passed && this.examples.length > MIN_EXAMPLES_COUNT2 && this.results.examplesPassed === this.results.examplesTested;
|
|
1572
|
+
return Promise.resolve(this.results);
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Validates examples by actually running PMD and checking results.
|
|
1576
|
+
* @param _maxConcurrency - Maximum number of examples to test concurrently.
|
|
1577
|
+
* @returns Promise resolving to validation results for each example.
|
|
1578
|
+
* @private
|
|
1579
|
+
*/
|
|
1580
|
+
async validateExamplesWithPMD(_maxConcurrency = DEFAULT_CONCURRENCY) {
|
|
1581
|
+
return this.validateExamplesWithPMDSequential();
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Validates examples sequentially (original implementation for compatibility).
|
|
1585
|
+
* @returns Promise resolving to validation results for each example.
|
|
1586
|
+
* @private
|
|
1587
|
+
*/
|
|
1588
|
+
async validateExamplesWithPMDSequential() {
|
|
1589
|
+
const EXAMPLE_INDEX_OFFSET = 1;
|
|
1590
|
+
const results = [];
|
|
1591
|
+
for (let i = 0; i < this.examples.length; i++) {
|
|
1592
|
+
const example = this.examples[i];
|
|
1593
|
+
const exampleIndex = i + EXAMPLE_INDEX_OFFSET;
|
|
1594
|
+
const testCaseResults = [];
|
|
1595
|
+
let passed = true;
|
|
1596
|
+
let actualViolations = 0;
|
|
1597
|
+
const MIN_VIOLATIONS_LENGTH = 0;
|
|
1598
|
+
if (example.violations.length > MIN_VIOLATIONS_LENGTH) {
|
|
1599
|
+
const violationTestFile = createTestFile({
|
|
1600
|
+
exampleContent: example.content,
|
|
1601
|
+
exampleIndex,
|
|
1602
|
+
includeValids: false,
|
|
1603
|
+
includeViolations: true
|
|
1604
|
+
});
|
|
1605
|
+
const testPassed = await this.runTestCase({
|
|
1606
|
+
exampleIndex,
|
|
1607
|
+
filePath: violationTestFile.filePath,
|
|
1608
|
+
testCaseResults,
|
|
1609
|
+
testType: "violation"
|
|
1610
|
+
});
|
|
1611
|
+
if (!testPassed) {
|
|
1612
|
+
passed = false;
|
|
1613
|
+
}
|
|
1614
|
+
try {
|
|
1615
|
+
const pmdResult = await runPMD(
|
|
1616
|
+
violationTestFile.filePath,
|
|
1617
|
+
this.ruleFilePath
|
|
1618
|
+
);
|
|
1619
|
+
if (pmdResult.success && pmdResult.data) {
|
|
1620
|
+
actualViolations += pmdResult.data.violations.length;
|
|
1621
|
+
}
|
|
1622
|
+
} catch {
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
const MIN_VALIDS_LENGTH = 0;
|
|
1626
|
+
if (example.valids.length > MIN_VALIDS_LENGTH) {
|
|
1627
|
+
const validTestFile = createTestFile({
|
|
1628
|
+
exampleContent: example.content,
|
|
1629
|
+
exampleIndex,
|
|
1630
|
+
includeValids: true,
|
|
1631
|
+
includeViolations: false
|
|
1632
|
+
});
|
|
1633
|
+
const testPassed = await this.runTestCase({
|
|
1634
|
+
exampleIndex,
|
|
1635
|
+
filePath: validTestFile.filePath,
|
|
1636
|
+
testCaseResults,
|
|
1637
|
+
testType: "valid"
|
|
1638
|
+
});
|
|
1639
|
+
if (!testPassed) {
|
|
1640
|
+
passed = false;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
results.push({
|
|
1644
|
+
actualViolations,
|
|
1645
|
+
exampleIndex,
|
|
1646
|
+
expectedValids: example.valids.length,
|
|
1647
|
+
expectedViolations: example.violations.length,
|
|
1648
|
+
passed,
|
|
1649
|
+
testCaseResults
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
return results;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Runs a single test case and records the result.
|
|
1656
|
+
* @param testCaseConfig - Configuration for the test case.
|
|
1657
|
+
* @param testCaseConfig.exampleIndex - 1-based index of the example being tested.
|
|
1658
|
+
* @param testCaseConfig.filePath - Path to the temporary test file.
|
|
1659
|
+
* @param testCaseConfig.testCaseResults - Array to append test case results to.
|
|
1660
|
+
* @param testCaseConfig.testType - Type of test ('valid' or 'violation').
|
|
1661
|
+
* @returns Promise resolving to whether the test passed.
|
|
1662
|
+
* @private
|
|
1663
|
+
*/
|
|
1664
|
+
async runTestCase(testCaseConfig) {
|
|
1665
|
+
const { exampleIndex, filePath, testCaseResults, testType } = testCaseConfig;
|
|
1666
|
+
try {
|
|
1667
|
+
const pmdResult = await runPMD(filePath, this.ruleFilePath);
|
|
1668
|
+
let passed = false;
|
|
1669
|
+
let lineNumber = void 0;
|
|
1670
|
+
const ZERO_VIOLATIONS_COUNT = 0;
|
|
1671
|
+
if (pmdResult.success && pmdResult.data) {
|
|
1672
|
+
if (testType === "violation") {
|
|
1673
|
+
passed = pmdResult.data.violations.length > ZERO_VIOLATIONS_COUNT;
|
|
1674
|
+
if (!passed) {
|
|
1675
|
+
lineNumber = this.findTestCaseLineNumber(
|
|
1676
|
+
exampleIndex,
|
|
1677
|
+
testType
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
} else {
|
|
1681
|
+
passed = pmdResult.data.violations.length === ZERO_VIOLATIONS_COUNT;
|
|
1682
|
+
if (!passed) {
|
|
1683
|
+
lineNumber = this.findTestCaseLineNumber(
|
|
1684
|
+
exampleIndex,
|
|
1685
|
+
testType
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
} else {
|
|
1690
|
+
passed = false;
|
|
1691
|
+
lineNumber = this.findTestCaseLineNumber(
|
|
1692
|
+
exampleIndex,
|
|
1693
|
+
testType
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
const testTypeLabel = testType === "violation" ? "Violation" : "Valid";
|
|
1697
|
+
testCaseResults.push({
|
|
1698
|
+
description: `${testTypeLabel} test for example ${String(exampleIndex)}`,
|
|
1699
|
+
exampleIndex,
|
|
1700
|
+
lineNumber,
|
|
1701
|
+
passed,
|
|
1702
|
+
testType
|
|
1703
|
+
});
|
|
1704
|
+
return passed;
|
|
1705
|
+
} catch {
|
|
1706
|
+
const lineNumber = this.findExampleLineNumber(exampleIndex);
|
|
1707
|
+
const testTypeLabel = testType === "violation" ? "Violation" : "Valid";
|
|
1708
|
+
testCaseResults.push({
|
|
1709
|
+
description: `${testTypeLabel} test for example ${String(exampleIndex)}`,
|
|
1710
|
+
exampleIndex,
|
|
1711
|
+
lineNumber,
|
|
1712
|
+
passed: false,
|
|
1713
|
+
testType
|
|
1714
|
+
});
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Finds the line number in the XML file for a specific test case within an example.
|
|
1720
|
+
* @param exampleIndex - 1-based example index.
|
|
1721
|
+
* @param testType - Type of test case ('valid' or 'violation').
|
|
1722
|
+
* @returns Line number in the XML file, or undefined if not found.
|
|
1723
|
+
* @private
|
|
1724
|
+
*/
|
|
1725
|
+
findTestCaseLineNumber(exampleIndex, testType) {
|
|
1726
|
+
try {
|
|
1727
|
+
const content = readFileSync3(this.ruleFilePath, "utf-8");
|
|
1728
|
+
const lines = content.split("\n");
|
|
1729
|
+
const NOT_FOUND_INDEX2 = -1;
|
|
1730
|
+
let exampleStart = NOT_FOUND_INDEX2;
|
|
1731
|
+
let exampleEnd = NOT_FOUND_INDEX2;
|
|
1732
|
+
let currentExampleIndex = 0;
|
|
1733
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1734
|
+
const line = lines[i];
|
|
1735
|
+
if (line.includes("<example>")) {
|
|
1736
|
+
currentExampleIndex++;
|
|
1737
|
+
exampleStart = currentExampleIndex === exampleIndex ? i : exampleStart;
|
|
1738
|
+
} else if (line.includes("</example>") && currentExampleIndex === exampleIndex) {
|
|
1739
|
+
exampleEnd = i;
|
|
1740
|
+
break;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
if (exampleStart === NOT_FOUND_INDEX2 || exampleEnd === NOT_FOUND_INDEX2) {
|
|
1744
|
+
return void 0;
|
|
1745
|
+
}
|
|
1746
|
+
const hasInlineMarkers = () => {
|
|
1747
|
+
for (let i = exampleStart; i <= exampleEnd; i++) {
|
|
1748
|
+
const line = lines[i];
|
|
1749
|
+
if (line.includes("// \u274C") || line.includes("// \u2705")) {
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return false;
|
|
1754
|
+
};
|
|
1755
|
+
const LINE_NUMBER_OFFSET2 = 1;
|
|
1756
|
+
const hasInline = hasInlineMarkers();
|
|
1757
|
+
for (let i = exampleStart; i <= exampleEnd; i++) {
|
|
1758
|
+
const line = lines[i];
|
|
1759
|
+
if (hasInline) {
|
|
1760
|
+
const inlineMarkerText = testType === "violation" ? "// \u274C" : "// \u2705";
|
|
1761
|
+
if (line.includes(inlineMarkerText)) {
|
|
1762
|
+
return i + LINE_NUMBER_OFFSET2;
|
|
1763
|
+
}
|
|
1764
|
+
} else {
|
|
1765
|
+
const sectionMarkerText = testType === "violation" ? "// Violation:" : "// Valid:";
|
|
1766
|
+
if (line.includes(sectionMarkerText)) {
|
|
1767
|
+
const NEXT_LINE_OFFSET = 1;
|
|
1768
|
+
for (let j = i + NEXT_LINE_OFFSET; j <= exampleEnd; j++) {
|
|
1769
|
+
const nextLineRaw = lines[j];
|
|
1770
|
+
const nextLine = nextLineRaw.trim();
|
|
1771
|
+
if (nextLine && !nextLine.startsWith("//") && !nextLine.startsWith("*/") && !nextLine.startsWith("/*") && !nextLine.startsWith("</") && !nextLine.startsWith("<")) {
|
|
1772
|
+
return j + LINE_NUMBER_OFFSET2;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
return void 0;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Finds the line number in the XML file for a given example index.
|
|
1784
|
+
* @param exampleIndex - 1-based example index.
|
|
1785
|
+
* @returns Line number in the XML file, or undefined if not found.
|
|
1786
|
+
* @private
|
|
1787
|
+
*/
|
|
1788
|
+
findExampleLineNumber(exampleIndex) {
|
|
1789
|
+
const content = readFileSync3(this.ruleFilePath, "utf-8");
|
|
1790
|
+
const lines = content.split("\n");
|
|
1791
|
+
let currentExampleIndex = 0;
|
|
1792
|
+
const LINE_NUMBER_OFFSET2 = 1;
|
|
1793
|
+
const NOT_FOUND_INDEX2 = -1;
|
|
1794
|
+
const foundIndex = lines.findIndex((line) => {
|
|
1795
|
+
if (line.includes("<example>")) {
|
|
1796
|
+
currentExampleIndex++;
|
|
1797
|
+
return currentExampleIndex === exampleIndex;
|
|
1798
|
+
}
|
|
1799
|
+
return false;
|
|
1800
|
+
});
|
|
1801
|
+
return foundIndex === NOT_FOUND_INDEX2 ? void 0 : foundIndex + LINE_NUMBER_OFFSET2;
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* Initializes an empty results object for a new test run.
|
|
1805
|
+
* @returns Initialized OverallTestResults object.
|
|
1806
|
+
* @private
|
|
1807
|
+
*/
|
|
1808
|
+
initializeResults() {
|
|
1809
|
+
void this.ruleFilePath;
|
|
1810
|
+
return {
|
|
1811
|
+
examplesPassed: MIN_EXAMPLES_COUNT2,
|
|
1812
|
+
examplesTested: MIN_EXAMPLES_COUNT2,
|
|
1813
|
+
hardcodedValues: [],
|
|
1814
|
+
ruleTriggersViolations: false,
|
|
1815
|
+
success: false,
|
|
1816
|
+
testResults: [],
|
|
1817
|
+
totalViolations: MIN_VIOLATIONS_COUNT,
|
|
1818
|
+
xpathCoverage: {
|
|
1819
|
+
coverage: [],
|
|
1820
|
+
overallSuccess: false,
|
|
1821
|
+
uncoveredBranches: []
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
|
|
1827
|
+
// src/utils/concurrency.ts
|
|
1828
|
+
async function limitConcurrency(tasks, maxConcurrency) {
|
|
1829
|
+
const results = new Array(tasks.length);
|
|
1830
|
+
const executing = [];
|
|
1831
|
+
let index = 0;
|
|
1832
|
+
const executeNext = async () => {
|
|
1833
|
+
const currentIndex = index++;
|
|
1834
|
+
const task = tasks[currentIndex];
|
|
1835
|
+
if (!task) {
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
try {
|
|
1839
|
+
results[currentIndex] = await task();
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
throw error;
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
for (let i = 0; i < Math.min(maxConcurrency, tasks.length); i++) {
|
|
1845
|
+
executing.push(executeNext());
|
|
1846
|
+
}
|
|
1847
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
1848
|
+
if (i >= maxConcurrency) {
|
|
1849
|
+
await executing[i % maxConcurrency];
|
|
1850
|
+
executing[i % maxConcurrency] = executeNext();
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
await Promise.all(executing);
|
|
1854
|
+
return results;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// src/coverage/trackCoverage.ts
|
|
1858
|
+
var CoverageTracker = class {
|
|
1859
|
+
constructor(ruleFilePath) {
|
|
1860
|
+
this.coverageData = {
|
|
1861
|
+
componentLines: /* @__PURE__ */ new Map(),
|
|
1862
|
+
filePath: ruleFilePath,
|
|
1863
|
+
xpathLines: /* @__PURE__ */ new Map()
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Records that an XPath line was executed.
|
|
1868
|
+
* @param lineNumber - Line number in the XML file.
|
|
1869
|
+
*/
|
|
1870
|
+
recordXPathLine(lineNumber) {
|
|
1871
|
+
const INITIAL_COUNT = 0;
|
|
1872
|
+
const INCREMENT = 1;
|
|
1873
|
+
const current = this.coverageData.xpathLines.get(lineNumber) ?? INITIAL_COUNT;
|
|
1874
|
+
this.coverageData.xpathLines.set(lineNumber, current + INCREMENT);
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Records that an XPath component line was executed.
|
|
1878
|
+
* @param lineNumber - Line number in the XML file.
|
|
1879
|
+
*/
|
|
1880
|
+
recordComponentLine(lineNumber) {
|
|
1881
|
+
const INITIAL_COUNT = 0;
|
|
1882
|
+
const INCREMENT = 1;
|
|
1883
|
+
const current = this.coverageData.componentLines.get(lineNumber) ?? INITIAL_COUNT;
|
|
1884
|
+
this.coverageData.componentLines.set(lineNumber, current + INCREMENT);
|
|
1885
|
+
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Gets the coverage data.
|
|
1888
|
+
* @returns Coverage data for this rule file.
|
|
1889
|
+
*/
|
|
1890
|
+
getCoverageData() {
|
|
1891
|
+
return this.coverageData;
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
|
|
1895
|
+
// src/coverage/generateLcov.ts
|
|
1896
|
+
import { writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
1897
|
+
import { dirname } from "path";
|
|
1898
|
+
function generateLcovReport(coverageData, outputPath) {
|
|
1899
|
+
const lcovContent = [];
|
|
1900
|
+
for (const data of coverageData) {
|
|
1901
|
+
lcovContent.push(`SF:${data.filePath}`);
|
|
1902
|
+
for (const [lineNumber, executionCount] of data.xpathLines) {
|
|
1903
|
+
lcovContent.push(`DA:${String(lineNumber)},${String(executionCount)}`);
|
|
1904
|
+
}
|
|
1905
|
+
for (const [lineNumber, executionCount] of data.componentLines) {
|
|
1906
|
+
lcovContent.push(`DA:${String(lineNumber)},${String(executionCount)}`);
|
|
1907
|
+
}
|
|
1908
|
+
lcovContent.push("end_of_record");
|
|
1909
|
+
}
|
|
1910
|
+
const outputDir = dirname(outputPath);
|
|
1911
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1912
|
+
const content = lcovContent.join("\n") + "\n";
|
|
1913
|
+
writeFileSync2(outputPath, content, "utf-8");
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// src/cli/main.ts
|
|
1917
|
+
var EXIT_CODE_SUCCESS = 0;
|
|
1918
|
+
var EXIT_CODE_ERROR = 1;
|
|
1919
|
+
var ARGV_SLICE_INDEX = 2;
|
|
1920
|
+
var MIN_ARGS_COUNT = 0;
|
|
1921
|
+
var MAX_ARGS_COUNT = 2;
|
|
1922
|
+
var FIRST_ARG_INDEX = 0;
|
|
1923
|
+
var SECOND_ARG_INDEX = 1;
|
|
1924
|
+
var REPEAT_CHAR_COUNT = 60;
|
|
1925
|
+
var MIN_FAILED_FILES_COUNT = 0;
|
|
1926
|
+
function findXmlFiles(directory) {
|
|
1927
|
+
const xmlFiles = [];
|
|
1928
|
+
const items = readdirSync(directory);
|
|
1929
|
+
for (const item of items) {
|
|
1930
|
+
const fullPath = resolve3(directory, item);
|
|
1931
|
+
const stat = statSync(fullPath);
|
|
1932
|
+
if (stat.isDirectory()) {
|
|
1933
|
+
xmlFiles.push(...findXmlFiles(fullPath));
|
|
1934
|
+
} else if (stat.isFile() && extname(fullPath).toLowerCase() === ".xml") {
|
|
1935
|
+
xmlFiles.push(fullPath);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
return xmlFiles;
|
|
1939
|
+
}
|
|
1940
|
+
async function testRuleFile(ruleFilePath, coverageTracker, maxConcurrency) {
|
|
1941
|
+
try {
|
|
1942
|
+
const tester = new RuleTester(ruleFilePath);
|
|
1943
|
+
const result = await tester.runCoverageTest(false, maxConcurrency);
|
|
1944
|
+
const hasCoverageTracker = coverageTracker !== null;
|
|
1945
|
+
const MIN_COVERED_LINES_COUNT = 0;
|
|
1946
|
+
const coveredLines = result.xpathCoverage.coveredLineNumbers;
|
|
1947
|
+
if (hasCoverageTracker && coveredLines && coveredLines.length > MIN_COVERED_LINES_COUNT) {
|
|
1948
|
+
for (const lineNumber of coveredLines) {
|
|
1949
|
+
coverageTracker.recordXPathLine(lineNumber);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
console.log(
|
|
1953
|
+
`
|
|
1954
|
+
\u{1F9EA} Testing rule: ${ruleFilePath}${coverageTracker ? " (with coverage)" : ""}
|
|
1955
|
+
`
|
|
1956
|
+
);
|
|
1957
|
+
const MIN_DETAILED_RESULTS_COUNT = 0;
|
|
1958
|
+
if (result.detailedTestResults && result.detailedTestResults.length > MIN_DETAILED_RESULTS_COUNT) {
|
|
1959
|
+
console.log("\u{1F4CB} Test Details:");
|
|
1960
|
+
for (const testResult of result.detailedTestResults) {
|
|
1961
|
+
const status = testResult.passed ? "\u2705" : "\u274C";
|
|
1962
|
+
const testType = testResult.testType === "violation" ? "Violation" : "Valid";
|
|
1963
|
+
const lineInfo = testResult.lineNumber !== void 0 ? ` Line: ${String(testResult.lineNumber)}` : "";
|
|
1964
|
+
console.log(
|
|
1965
|
+
` - Example ${String(testResult.exampleIndex)} Test: ${testType} ${status}${lineInfo}`
|
|
1966
|
+
);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
const MIN_COUNT3 = 0;
|
|
1970
|
+
const INDEX_OFFSET3 = 1;
|
|
1971
|
+
if (result.success) {
|
|
1972
|
+
console.log("\n\u{1F4CA} Test Summary:");
|
|
1973
|
+
console.log(` Examples tested: ${String(result.examplesTested)}`);
|
|
1974
|
+
console.log(` Examples passed: ${String(result.examplesPassed)}`);
|
|
1975
|
+
console.log(
|
|
1976
|
+
` Total violations: ${String(result.totalViolations)}`
|
|
1977
|
+
);
|
|
1978
|
+
console.log(
|
|
1979
|
+
` Rule triggers violations: ${result.ruleTriggersViolations ? "\u2705 Yes" : "\u274C No"}`
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
console.log("\n\u{1F50D} XPath Coverage:");
|
|
1983
|
+
if (result.xpathCoverage.overallSuccess) {
|
|
1984
|
+
console.log(" Status: \u2705 Complete");
|
|
1985
|
+
} else {
|
|
1986
|
+
const INCOMPLETE_STATUS_MESSAGE = " Status: \u26A0\uFE0F Incomplete";
|
|
1987
|
+
console.log(INCOMPLETE_STATUS_MESSAGE);
|
|
1988
|
+
}
|
|
1989
|
+
if (result.xpathCoverage.coverage.length > MIN_COUNT3) {
|
|
1990
|
+
console.log(
|
|
1991
|
+
` Coverage items: ${String(result.xpathCoverage.coverage.length)}`
|
|
1992
|
+
);
|
|
1993
|
+
result.xpathCoverage.coverage.forEach(
|
|
1994
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameters for forEach
|
|
1995
|
+
(coverage, index) => {
|
|
1996
|
+
const itemNumber = index + INDEX_OFFSET3;
|
|
1997
|
+
const status = coverage.success ? "\u2705" : coverage.evidence.some(
|
|
1998
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameter for some
|
|
1999
|
+
(evidence) => evidence.count > MIN_COUNT3 && evidence.count < evidence.required
|
|
2000
|
+
) ? "\u26A0\uFE0F" : "\u274C";
|
|
2001
|
+
console.log(
|
|
2002
|
+
` ${String(itemNumber)}. ${status} ${coverage.message}`
|
|
2003
|
+
);
|
|
2004
|
+
if (coverage.evidence.length > MIN_COUNT3) {
|
|
2005
|
+
coverage.evidence.forEach(
|
|
2006
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameters for forEach
|
|
2007
|
+
(evidence) => {
|
|
2008
|
+
const { description } = evidence;
|
|
2009
|
+
if (description.length > MIN_COUNT3) {
|
|
2010
|
+
if (description.includes("\n")) {
|
|
2011
|
+
description.split("\n").forEach(
|
|
2012
|
+
(line) => {
|
|
2013
|
+
console.log(
|
|
2014
|
+
` ${line}`
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
);
|
|
2018
|
+
} else {
|
|
2019
|
+
console.log(` ${description}`);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
if (result.hardcodedValues.length > MIN_COUNT3) {
|
|
2029
|
+
console.log(
|
|
2030
|
+
`
|
|
2031
|
+
\u26A0\uFE0F Hardcoded values found: ${String(result.hardcodedValues.length)}`
|
|
2032
|
+
);
|
|
2033
|
+
result.hardcodedValues.forEach(
|
|
2034
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameters for forEach
|
|
2035
|
+
(issue) => {
|
|
2036
|
+
console.log(
|
|
2037
|
+
` - ${issue.type}: ${issue.value} (${issue.severity})`
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
const isCoverageIncomplete = !result.xpathCoverage.overallSuccess;
|
|
2043
|
+
if (result.success && !isCoverageIncomplete) {
|
|
2044
|
+
console.log("\n\u2705 All tests passed!");
|
|
2045
|
+
} else if (isCoverageIncomplete) {
|
|
2046
|
+
console.log("\n\u274C Tests failed, incomplete coverage");
|
|
2047
|
+
}
|
|
2048
|
+
tester.cleanup();
|
|
2049
|
+
const coverageData = coverageTracker !== null ? coverageTracker.getCoverageData() : void 0;
|
|
2050
|
+
return {
|
|
2051
|
+
coverageData,
|
|
2052
|
+
filePath: ruleFilePath,
|
|
2053
|
+
success: result.success
|
|
2054
|
+
};
|
|
2055
|
+
} catch (error) {
|
|
2056
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2057
|
+
console.error(`
|
|
2058
|
+
\u274C Error testing ${ruleFilePath}: ${errorMessage}`);
|
|
2059
|
+
return { error: errorMessage, filePath: ruleFilePath, success: false };
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
async function main() {
|
|
2063
|
+
const args = argv.slice(ARGV_SLICE_INDEX);
|
|
2064
|
+
if (args.length === MIN_ARGS_COUNT || args.length > MAX_ARGS_COUNT) {
|
|
2065
|
+
console.log("Usage: test-pmd-rule <rule.xml|directory> [--coverage]");
|
|
2066
|
+
console.log(
|
|
2067
|
+
"\nThis tool tests PMD rules using examples embedded in XML rule files."
|
|
2068
|
+
);
|
|
2069
|
+
console.log("\nArguments:");
|
|
2070
|
+
console.log(
|
|
2071
|
+
" <rule.xml|directory> Path to XML rule file or directory containing XML files"
|
|
2072
|
+
);
|
|
2073
|
+
console.log(
|
|
2074
|
+
" --coverage Generate LCOV coverage report in coverage/lcov.info"
|
|
2075
|
+
);
|
|
2076
|
+
console.log("\nRequirements:");
|
|
2077
|
+
console.log("- PMD CLI installed and in PATH");
|
|
2078
|
+
console.log("- Node.js 25+");
|
|
2079
|
+
process.exit(EXIT_CODE_ERROR);
|
|
2080
|
+
}
|
|
2081
|
+
const pathArg = args[FIRST_ARG_INDEX];
|
|
2082
|
+
if (typeof pathArg !== "string") {
|
|
2083
|
+
console.error("\u274C Invalid path argument");
|
|
2084
|
+
process.exit(EXIT_CODE_ERROR);
|
|
2085
|
+
}
|
|
2086
|
+
const hasCoverageFlag = args.length > SECOND_ARG_INDEX && args[SECOND_ARG_INDEX] === "--coverage";
|
|
2087
|
+
if (!existsSync2(pathArg)) {
|
|
2088
|
+
console.error(`\u274C Path not found: ${pathArg}`);
|
|
2089
|
+
process.exit(EXIT_CODE_ERROR);
|
|
2090
|
+
}
|
|
2091
|
+
const stat = statSync(pathArg);
|
|
2092
|
+
const xmlFiles = [];
|
|
2093
|
+
if (stat.isFile()) {
|
|
2094
|
+
if (!pathArg.endsWith(".xml")) {
|
|
2095
|
+
console.error("\u274C File must be an XML rule file (.xml)");
|
|
2096
|
+
process.exit(EXIT_CODE_ERROR);
|
|
2097
|
+
}
|
|
2098
|
+
xmlFiles.push(pathArg);
|
|
2099
|
+
} else if (stat.isDirectory()) {
|
|
2100
|
+
xmlFiles.push(...findXmlFiles(pathArg));
|
|
2101
|
+
const MIN_XML_FILES_COUNT = 0;
|
|
2102
|
+
if (xmlFiles.length === MIN_XML_FILES_COUNT) {
|
|
2103
|
+
console.error(`\u274C No XML files found in directory: ${pathArg}`);
|
|
2104
|
+
process.exit(EXIT_CODE_ERROR);
|
|
2105
|
+
}
|
|
2106
|
+
} else {
|
|
2107
|
+
console.error(`\u274C Path is neither a file nor directory: ${pathArg}`);
|
|
2108
|
+
process.exit(EXIT_CODE_ERROR);
|
|
2109
|
+
}
|
|
2110
|
+
const cpuCount = cpus().length;
|
|
2111
|
+
const maxFileConcurrency = Math.min(xmlFiles.length, cpuCount);
|
|
2112
|
+
const maxExampleConcurrency = cpuCount;
|
|
2113
|
+
console.log(
|
|
2114
|
+
`
|
|
2115
|
+
\u{1F680} Processing ${String(xmlFiles.length)} rule file(s) with ${String(maxFileConcurrency)} parallel workers`
|
|
2116
|
+
);
|
|
2117
|
+
console.log(
|
|
2118
|
+
` Each file will test examples with up to ${String(maxExampleConcurrency)} parallel workers
|
|
2119
|
+
`
|
|
2120
|
+
);
|
|
2121
|
+
const coverageTrackers = hasCoverageFlag ? /* @__PURE__ */ new Map() : null;
|
|
2122
|
+
const tasks = xmlFiles.map(
|
|
2123
|
+
(filePath) => async () => {
|
|
2124
|
+
const tracker = coverageTrackers !== null ? coverageTrackers.get(filePath) ?? new CoverageTracker(filePath) : null;
|
|
2125
|
+
if (tracker !== null && coverageTrackers !== null) {
|
|
2126
|
+
coverageTrackers.set(filePath, tracker);
|
|
2127
|
+
}
|
|
2128
|
+
return testRuleFile(filePath, tracker, maxExampleConcurrency);
|
|
2129
|
+
}
|
|
2130
|
+
);
|
|
2131
|
+
const results = await limitConcurrency(tasks, maxFileConcurrency);
|
|
2132
|
+
const successfulFiles = results.filter(
|
|
2133
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameter
|
|
2134
|
+
(r) => r.success
|
|
2135
|
+
).length;
|
|
2136
|
+
const failedFiles = results.filter(
|
|
2137
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameter
|
|
2138
|
+
(r) => !r.success
|
|
2139
|
+
).length;
|
|
2140
|
+
console.log("\n" + "=".repeat(REPEAT_CHAR_COUNT));
|
|
2141
|
+
console.log("\u{1F3AF} OVERALL RESULTS");
|
|
2142
|
+
console.log("=".repeat(REPEAT_CHAR_COUNT));
|
|
2143
|
+
console.log(`Total files processed: ${String(xmlFiles.length)}`);
|
|
2144
|
+
console.log(`Successful: ${String(successfulFiles)}`);
|
|
2145
|
+
console.log(`Failed: ${String(failedFiles)}`);
|
|
2146
|
+
if (failedFiles > MIN_FAILED_FILES_COUNT) {
|
|
2147
|
+
console.log("\n\u274C Failed files:");
|
|
2148
|
+
results.filter((r) => !r.success).forEach((result) => {
|
|
2149
|
+
const errorMessage = result.error ?? "";
|
|
2150
|
+
const MIN_ERROR_LENGTH = 0;
|
|
2151
|
+
const errorSuffix = errorMessage.length > MIN_ERROR_LENGTH ? `: ${errorMessage}` : "";
|
|
2152
|
+
console.log(` - ${result.filePath}${errorSuffix}`);
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
if (hasCoverageFlag && coverageTrackers !== null) {
|
|
2156
|
+
const coverageData = Array.from(
|
|
2157
|
+
coverageTrackers.values()
|
|
2158
|
+
).map(
|
|
2159
|
+
(tracker) => tracker.getCoverageData()
|
|
2160
|
+
);
|
|
2161
|
+
try {
|
|
2162
|
+
generateLcovReport(coverageData, "coverage/lcov.info");
|
|
2163
|
+
console.log("\n\u{1F4CA} Coverage report generated: coverage/lcov.info");
|
|
2164
|
+
} catch (error) {
|
|
2165
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2166
|
+
console.error(
|
|
2167
|
+
`
|
|
2168
|
+
\u274C Error generating coverage report: ${errorMessage}`
|
|
2169
|
+
);
|
|
2170
|
+
process.exit(EXIT_CODE_ERROR);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
process.exit(
|
|
2174
|
+
failedFiles === MIN_FAILED_FILES_COUNT ? EXIT_CODE_SUCCESS : EXIT_CODE_ERROR
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
process.on("uncaughtException", (error) => {
|
|
2178
|
+
console.error(`Unexpected error: ${error.message}`);
|
|
2179
|
+
process.exit(EXIT_CODE_ERROR);
|
|
2180
|
+
});
|
|
2181
|
+
process.on(
|
|
2182
|
+
"unhandledRejection",
|
|
2183
|
+
(_reason, _promise) => {
|
|
2184
|
+
const reasonString = typeof _reason === "string" ? _reason : _reason instanceof Error ? _reason.message : JSON.stringify(_reason);
|
|
2185
|
+
const promiseString = "[Promise]";
|
|
2186
|
+
console.error(
|
|
2187
|
+
`Unhandled Rejection at: ${promiseString}, reason: ${reasonString}`
|
|
2188
|
+
);
|
|
2189
|
+
process.exit(EXIT_CODE_ERROR);
|
|
2190
|
+
}
|
|
2191
|
+
);
|
|
2192
|
+
main().catch((error) => {
|
|
2193
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2194
|
+
console.error(`Unexpected error: ${errorMessage}`);
|
|
2195
|
+
process.exit(EXIT_CODE_ERROR);
|
|
2196
|
+
});
|
|
2197
|
+
//# sourceMappingURL=test-pmd-rule.js.map
|