test-pmd-rule 1.0.2 → 1.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/dist/test-pmd-rule.js +770 -75
- package/dist/test-pmd-rule.js.map +4 -4
- package/package.json +1 -1
package/dist/test-pmd-rule.js
CHANGED
|
@@ -7,7 +7,7 @@ import { argv } from "process";
|
|
|
7
7
|
import { cpus } from "os";
|
|
8
8
|
|
|
9
9
|
// src/tester/RuleTester.ts
|
|
10
|
-
import { readFileSync as
|
|
10
|
+
import { readFileSync as readFileSync4, existsSync } from "fs";
|
|
11
11
|
import { DOMParser as DOMParser3 } from "@xmldom/xmldom";
|
|
12
12
|
|
|
13
13
|
// src/xpath/extractXPath.ts
|
|
@@ -63,8 +63,8 @@ import { readFileSync as readFileSync2 } from "fs";
|
|
|
63
63
|
|
|
64
64
|
// src/xpath/extractors/extractNodeTypes.ts
|
|
65
65
|
function extractNodeTypes(xpath) {
|
|
66
|
-
const
|
|
67
|
-
if (xpath.length ===
|
|
66
|
+
const MIN_STRING_LENGTH8 = 0;
|
|
67
|
+
if (xpath.length === MIN_STRING_LENGTH8) return [];
|
|
68
68
|
const nodeTypes = /* @__PURE__ */ new Set();
|
|
69
69
|
const nodeTypeMatches1 = xpath.matchAll(
|
|
70
70
|
/(?:\.\/\/|\/\/|\s|\/|\(|\[|,|\|)([A-Z][a-zA-Z]*(?:Statement|Expression|Declaration|Node|Block))(?=\s|$|\[|\(|\/|\)|,|\||]|or|and|not|return|let)/g
|
|
@@ -152,8 +152,8 @@ function extractAttributes(xpath) {
|
|
|
152
152
|
|
|
153
153
|
// src/xpath/extractors/extractConditionals.ts
|
|
154
154
|
function extractConditionals(xpath) {
|
|
155
|
-
const
|
|
156
|
-
if (xpath.length ===
|
|
155
|
+
const MIN_STRING_LENGTH8 = 0;
|
|
156
|
+
if (xpath.length === MIN_STRING_LENGTH8) return [];
|
|
157
157
|
const conditionals = [];
|
|
158
158
|
const MATCH_INDEX3 = 1;
|
|
159
159
|
const notMatches = xpath.matchAll(/not\s*\(([^)]+)\)/g);
|
|
@@ -1022,6 +1022,34 @@ import tmp from "tmp";
|
|
|
1022
1022
|
var EMPTY_LENGTH = 0;
|
|
1023
1023
|
var FIRST_CAPTURE_GROUP_INDEX = 1;
|
|
1024
1024
|
var SECOND_CAPTURE_GROUP_INDEX = 2;
|
|
1025
|
+
var TAB_CHAR = " ";
|
|
1026
|
+
var SPACE_CHAR = " ";
|
|
1027
|
+
var DEFAULT_INDENT = " ";
|
|
1028
|
+
var FIRST_ELEMENT_INDEX2 = 0;
|
|
1029
|
+
function removeInlineMarkers(line) {
|
|
1030
|
+
if (line.includes("// \u274C")) {
|
|
1031
|
+
const splitResult = line.split("// \u274C");
|
|
1032
|
+
const codeLine = splitResult[FIRST_ELEMENT_INDEX2];
|
|
1033
|
+
return codeLine.replace(/\s+$/, "");
|
|
1034
|
+
}
|
|
1035
|
+
if (line.includes("// \u2705")) {
|
|
1036
|
+
const splitResult = line.split("// \u2705");
|
|
1037
|
+
const codeLine = splitResult[FIRST_ELEMENT_INDEX2];
|
|
1038
|
+
return codeLine.replace(/\s+$/, "");
|
|
1039
|
+
}
|
|
1040
|
+
return line;
|
|
1041
|
+
}
|
|
1042
|
+
function formatMethodDeclarationIndent(methodDecl) {
|
|
1043
|
+
const startsWithTab = methodDecl.startsWith(TAB_CHAR);
|
|
1044
|
+
if (startsWithTab) {
|
|
1045
|
+
return methodDecl;
|
|
1046
|
+
}
|
|
1047
|
+
const startsWithSpace = methodDecl.startsWith(SPACE_CHAR);
|
|
1048
|
+
if (startsWithSpace) {
|
|
1049
|
+
return methodDecl;
|
|
1050
|
+
}
|
|
1051
|
+
return `${DEFAULT_INDENT}${methodDecl}`;
|
|
1052
|
+
}
|
|
1025
1053
|
function inferReturnType(code, methodName) {
|
|
1026
1054
|
if (new RegExp(`\\b${methodName}\\s*\\(\\s*\\)\\s*[><=!]+\\s*\\d+`).test(
|
|
1027
1055
|
code
|
|
@@ -1256,8 +1284,15 @@ function createTestFile({
|
|
|
1256
1284
|
const extractedCode = [];
|
|
1257
1285
|
let insideClass = false;
|
|
1258
1286
|
let classBraceDepth = ZERO_BRACE_DEPTH;
|
|
1287
|
+
const INITIAL_BRACE_DEPTH = 1;
|
|
1288
|
+
let methodBraceDepth = ZERO_BRACE_DEPTH;
|
|
1259
1289
|
let currentMode = null;
|
|
1260
|
-
|
|
1290
|
+
let insideMethod = false;
|
|
1291
|
+
let methodDeclaration = null;
|
|
1292
|
+
let methodDeclarationOriginal = null;
|
|
1293
|
+
let hasIncludedMethodContent = false;
|
|
1294
|
+
for (let lineIndex = 0; lineIndex < exampleLines.length; lineIndex++) {
|
|
1295
|
+
const line = exampleLines[lineIndex];
|
|
1261
1296
|
const trimmed = line.trim();
|
|
1262
1297
|
if (trimmed.includes("// \u274C")) {
|
|
1263
1298
|
currentMode = "violation";
|
|
@@ -1270,7 +1305,6 @@ function createTestFile({
|
|
|
1270
1305
|
}
|
|
1271
1306
|
const shouldInclude = !trimmed.startsWith("//") && trimmed.length > EMPTY_LENGTH && (includeViolations && currentMode === "violation" || includeValids && currentMode === "valid");
|
|
1272
1307
|
if (trimmed.startsWith("public class ") || trimmed.startsWith("class ") || trimmed.startsWith("private class ")) {
|
|
1273
|
-
const INITIAL_BRACE_DEPTH = 1;
|
|
1274
1308
|
const CLASS_PREFIX_GROUP_INDEX = 1;
|
|
1275
1309
|
if (classBraceDepth === ZERO_BRACE_DEPTH) {
|
|
1276
1310
|
const classRegex = /^(public\s+|private\s+)?class\s+(\w+)/;
|
|
@@ -1290,33 +1324,64 @@ function createTestFile({
|
|
|
1290
1324
|
const closeBracesMatch = line.match(/}/g);
|
|
1291
1325
|
const openBraces = openBracesMatch ? openBracesMatch.length : ZERO_BRACE_DEPTH;
|
|
1292
1326
|
const closeBraces = closeBracesMatch ? closeBracesMatch.length : ZERO_BRACE_DEPTH;
|
|
1327
|
+
const prevClassBraceDepth = classBraceDepth;
|
|
1293
1328
|
classBraceDepth += openBraces - closeBraces;
|
|
1329
|
+
const methodRegex = /\w+\s*\(/;
|
|
1330
|
+
const methodMatch = methodRegex.exec(trimmed);
|
|
1331
|
+
const hasMethodPattern = methodMatch !== null;
|
|
1332
|
+
const isMethodDeclaration = prevClassBraceDepth === INITIAL_BRACE_DEPTH && classBraceDepth > INITIAL_BRACE_DEPTH && !trimmed.startsWith("{") && trimmed.includes("{") && (trimmed.includes("void") || trimmed.includes("(") || hasMethodPattern);
|
|
1333
|
+
if (isMethodDeclaration) {
|
|
1334
|
+
insideMethod = true;
|
|
1335
|
+
methodBraceDepth = classBraceDepth - INITIAL_BRACE_DEPTH;
|
|
1336
|
+
methodDeclaration = trimmed;
|
|
1337
|
+
methodDeclarationOriginal = line;
|
|
1338
|
+
hasIncludedMethodContent = false;
|
|
1339
|
+
}
|
|
1340
|
+
if (insideMethod) {
|
|
1341
|
+
methodBraceDepth += openBraces - closeBraces;
|
|
1342
|
+
if (methodBraceDepth <= ZERO_BRACE_DEPTH) {
|
|
1343
|
+
insideMethod = false;
|
|
1344
|
+
methodDeclaration = null;
|
|
1345
|
+
methodDeclarationOriginal = null;
|
|
1346
|
+
hasIncludedMethodContent = false;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1294
1349
|
if (shouldInclude || trimmed === "{" || trimmed === "}") {
|
|
1295
|
-
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1350
|
+
const hasMethodDeclaration = methodDeclaration !== null && methodDeclarationOriginal !== null;
|
|
1351
|
+
const markerRegex = /\s*\/\/\s*[❌✅].*$/;
|
|
1352
|
+
const trimmedWithoutMarker = trimmed.replace(markerRegex, "").trim();
|
|
1353
|
+
const isMethodDeclarationLine = insideMethod && hasMethodDeclaration && (trimmed === methodDeclaration || trimmedWithoutMarker === methodDeclaration);
|
|
1354
|
+
if (shouldInclude && insideMethod && hasMethodDeclaration && !hasIncludedMethodContent) {
|
|
1355
|
+
if (isMethodDeclarationLine) {
|
|
1356
|
+
const codeLine = removeInlineMarkers(line);
|
|
1357
|
+
extractedCode.push(codeLine);
|
|
1358
|
+
hasIncludedMethodContent = true;
|
|
1359
|
+
} else {
|
|
1360
|
+
const methodDeclOriginal = methodDeclarationOriginal;
|
|
1361
|
+
const methodDeclWithIndent = formatMethodDeclarationIndent(
|
|
1362
|
+
methodDeclOriginal
|
|
1363
|
+
);
|
|
1364
|
+
extractedCode.push(methodDeclWithIndent);
|
|
1365
|
+
const codeLine = removeInlineMarkers(line);
|
|
1366
|
+
extractedCode.push(codeLine);
|
|
1367
|
+
hasIncludedMethodContent = true;
|
|
1368
|
+
}
|
|
1369
|
+
} else if (shouldInclude && !isMethodDeclarationLine) {
|
|
1370
|
+
const codeLine = removeInlineMarkers(line);
|
|
1371
|
+
extractedCode.push(codeLine);
|
|
1372
|
+
} else {
|
|
1373
|
+
extractedCode.push(line);
|
|
1303
1374
|
}
|
|
1304
|
-
extractedCode.push(codeLine);
|
|
1305
1375
|
}
|
|
1306
1376
|
if (classBraceDepth <= ZERO_BRACE_DEPTH) {
|
|
1307
1377
|
insideClass = false;
|
|
1308
1378
|
classBraceDepth = ZERO_BRACE_DEPTH;
|
|
1379
|
+
insideMethod = false;
|
|
1380
|
+
methodDeclaration = null;
|
|
1381
|
+
methodDeclarationOriginal = null;
|
|
1309
1382
|
}
|
|
1310
1383
|
} else if (shouldInclude) {
|
|
1311
|
-
|
|
1312
|
-
const FIRST_ELEMENT_INDEX2 = 0;
|
|
1313
|
-
if (line.includes("// \u274C")) {
|
|
1314
|
-
const splitResult = line.split("// \u274C");
|
|
1315
|
-
codeLine = splitResult[FIRST_ELEMENT_INDEX2].trim();
|
|
1316
|
-
} else if (line.includes("// \u2705")) {
|
|
1317
|
-
const splitResult = line.split("// \u2705");
|
|
1318
|
-
codeLine = splitResult[FIRST_ELEMENT_INDEX2].trim();
|
|
1319
|
-
}
|
|
1384
|
+
const codeLine = removeInlineMarkers(line).trim();
|
|
1320
1385
|
extractedCode.push(codeLine);
|
|
1321
1386
|
}
|
|
1322
1387
|
}
|
|
@@ -1734,7 +1799,7 @@ function checkDuplicates(examples) {
|
|
|
1734
1799
|
|
|
1735
1800
|
// src/tester/qualityChecks.ts
|
|
1736
1801
|
var MIN_ISSUES_COUNT = 0;
|
|
1737
|
-
function runQualityChecks(ruleMetadata, examples) {
|
|
1802
|
+
function runQualityChecks(ruleFilePath, ruleMetadata, examples) {
|
|
1738
1803
|
const issues = [];
|
|
1739
1804
|
const warnings = [];
|
|
1740
1805
|
const metadataResult = checkRuleMetadata(ruleMetadata);
|
|
@@ -1753,6 +1818,595 @@ function runQualityChecks(ruleMetadata, examples) {
|
|
|
1753
1818
|
};
|
|
1754
1819
|
}
|
|
1755
1820
|
|
|
1821
|
+
// src/tester/quality/checkQualityChecks.ts
|
|
1822
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1823
|
+
|
|
1824
|
+
// src/xpath/extractors/extractLetVariables.ts
|
|
1825
|
+
var MIN_STRING_LENGTH5 = 0;
|
|
1826
|
+
var MATCH_FULL_INDEX = 0;
|
|
1827
|
+
var NOT_FOUND_INDEX2 = -1;
|
|
1828
|
+
var QUOTE_MATCH_GROUP_INDEX = 1;
|
|
1829
|
+
var VALUE_MATCH_GROUP_INDEX = 2;
|
|
1830
|
+
var ZERO_COUNT = 0;
|
|
1831
|
+
function extractLetVariables(xpath) {
|
|
1832
|
+
if (xpath.length === MIN_STRING_LENGTH5) return [];
|
|
1833
|
+
const variables = [];
|
|
1834
|
+
const letStartMatch = /let\s+/i.exec(xpath);
|
|
1835
|
+
if (letStartMatch?.index === void 0) {
|
|
1836
|
+
return variables;
|
|
1837
|
+
}
|
|
1838
|
+
const letStartIndex = letStartMatch.index + letStartMatch[MATCH_FULL_INDEX].length;
|
|
1839
|
+
let inSingleQuote = false;
|
|
1840
|
+
let inDoubleQuote = false;
|
|
1841
|
+
let parenDepth = 0;
|
|
1842
|
+
let declarationsEnd = NOT_FOUND_INDEX2;
|
|
1843
|
+
for (let i = letStartIndex; i < xpath.length; i++) {
|
|
1844
|
+
const char = xpath.charAt(i);
|
|
1845
|
+
if (char === "'" && !inDoubleQuote) {
|
|
1846
|
+
inSingleQuote = !inSingleQuote;
|
|
1847
|
+
} else if (char === '"' && !inSingleQuote) {
|
|
1848
|
+
inDoubleQuote = !inDoubleQuote;
|
|
1849
|
+
} else if (!inSingleQuote && !inDoubleQuote) {
|
|
1850
|
+
if (char === "(") {
|
|
1851
|
+
parenDepth++;
|
|
1852
|
+
} else if (char === ")") {
|
|
1853
|
+
parenDepth--;
|
|
1854
|
+
} else if (parenDepth === ZERO_COUNT && xpath.substring(i).toLowerCase().startsWith("return")) {
|
|
1855
|
+
declarationsEnd = i;
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (declarationsEnd === NOT_FOUND_INDEX2) {
|
|
1861
|
+
return variables;
|
|
1862
|
+
}
|
|
1863
|
+
const declarationsPart = xpath.substring(letStartIndex, declarationsEnd);
|
|
1864
|
+
let inQuote2 = false;
|
|
1865
|
+
let inDoubleQuote2 = false;
|
|
1866
|
+
let parenDepth2 = 0;
|
|
1867
|
+
let currentVarStart = NOT_FOUND_INDEX2;
|
|
1868
|
+
const varDeclarations = [];
|
|
1869
|
+
for (let i = 0; i < declarationsPart.length; i++) {
|
|
1870
|
+
const char = declarationsPart.charAt(i);
|
|
1871
|
+
if (char === "'" && !inDoubleQuote2) {
|
|
1872
|
+
inQuote2 = !inQuote2;
|
|
1873
|
+
} else if (char === '"' && !inQuote2) {
|
|
1874
|
+
inDoubleQuote2 = !inDoubleQuote2;
|
|
1875
|
+
} else if (!inQuote2 && !inDoubleQuote2) {
|
|
1876
|
+
if (char === "(") {
|
|
1877
|
+
parenDepth2++;
|
|
1878
|
+
} else if (char === ")") {
|
|
1879
|
+
parenDepth2--;
|
|
1880
|
+
} else if (char === "$" && parenDepth2 === ZERO_COUNT) {
|
|
1881
|
+
if (currentVarStart >= ZERO_COUNT) {
|
|
1882
|
+
varDeclarations.push({ end: i, start: currentVarStart });
|
|
1883
|
+
}
|
|
1884
|
+
currentVarStart = i;
|
|
1885
|
+
} else if (char === "," && parenDepth2 === ZERO_COUNT && currentVarStart >= ZERO_COUNT) {
|
|
1886
|
+
varDeclarations.push({ end: i, start: currentVarStart });
|
|
1887
|
+
currentVarStart = NOT_FOUND_INDEX2;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
if (currentVarStart >= ZERO_COUNT) {
|
|
1892
|
+
varDeclarations.push({
|
|
1893
|
+
end: declarationsPart.length,
|
|
1894
|
+
start: currentVarStart
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
for (const decl of varDeclarations) {
|
|
1898
|
+
const declText = declarationsPart.substring(decl.start, decl.end).trim();
|
|
1899
|
+
const equalsMatch = /\$([a-zA-Z_][a-zA-Z0-9_]*)\s*:?=\s*(.+)/s.exec(
|
|
1900
|
+
declText
|
|
1901
|
+
);
|
|
1902
|
+
if (equalsMatch) {
|
|
1903
|
+
const varName = `$${equalsMatch[QUOTE_MATCH_GROUP_INDEX]}`;
|
|
1904
|
+
const varValue = equalsMatch[VALUE_MATCH_GROUP_INDEX].trim();
|
|
1905
|
+
variables.push({
|
|
1906
|
+
name: varName,
|
|
1907
|
+
value: varValue
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
return variables;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// src/xpath/extractors/extractHardcodedValues.ts
|
|
1915
|
+
var MIN_STRING_LENGTH6 = 0;
|
|
1916
|
+
var MATCH_FULL_INDEX2 = 0;
|
|
1917
|
+
var NUMBER_VALUE_GROUP_INDEX = 1;
|
|
1918
|
+
var QUOTE_GROUP_INDEX = 1;
|
|
1919
|
+
var STRING_VALUE_GROUP_INDEX = 2;
|
|
1920
|
+
var ZERO_COUNT2 = 0;
|
|
1921
|
+
function extractHardcodedValues(xpath) {
|
|
1922
|
+
if (xpath.length === MIN_STRING_LENGTH6) return [];
|
|
1923
|
+
const values = [];
|
|
1924
|
+
let letStart = -1;
|
|
1925
|
+
let letEnd = -1;
|
|
1926
|
+
const letStartMatch = /let\s+/i.exec(xpath);
|
|
1927
|
+
if (letStartMatch?.index !== void 0) {
|
|
1928
|
+
letStart = letStartMatch.index;
|
|
1929
|
+
const letStartIndex = letStartMatch.index + letStartMatch[MATCH_FULL_INDEX2].length;
|
|
1930
|
+
let inSingleQuote = false;
|
|
1931
|
+
let inDoubleQuote = false;
|
|
1932
|
+
let parenDepth = 0;
|
|
1933
|
+
for (let i = letStartIndex; i < xpath.length; i++) {
|
|
1934
|
+
const char = xpath.charAt(i);
|
|
1935
|
+
if (char === "'" && !inDoubleQuote) {
|
|
1936
|
+
inSingleQuote = !inSingleQuote;
|
|
1937
|
+
} else if (char === '"' && !inSingleQuote) {
|
|
1938
|
+
inDoubleQuote = !inDoubleQuote;
|
|
1939
|
+
} else if (!inSingleQuote && !inDoubleQuote) {
|
|
1940
|
+
if (char === "(") {
|
|
1941
|
+
parenDepth++;
|
|
1942
|
+
} else if (char === ")") {
|
|
1943
|
+
parenDepth--;
|
|
1944
|
+
} else if (parenDepth === ZERO_COUNT2 && xpath.substring(i).toLowerCase().startsWith("return")) {
|
|
1945
|
+
const returnEnd = i + "return".length;
|
|
1946
|
+
letEnd = returnEnd;
|
|
1947
|
+
break;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
const stringPattern = /(['"])([^'"]*)\1/g;
|
|
1953
|
+
let stringMatch = null;
|
|
1954
|
+
while ((stringMatch = stringPattern.exec(xpath)) !== null) {
|
|
1955
|
+
if (letStart >= ZERO_COUNT2 && stringMatch.index >= letStart && stringMatch.index < letEnd) {
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
const stringValue = stringMatch[STRING_VALUE_GROUP_INDEX];
|
|
1959
|
+
if (stringValue.length === MIN_STRING_LENGTH6) continue;
|
|
1960
|
+
values.push({
|
|
1961
|
+
position: stringMatch.index,
|
|
1962
|
+
type: "string",
|
|
1963
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Regex capture groups ensure these exist
|
|
1964
|
+
value: `${stringMatch[QUOTE_GROUP_INDEX]}${stringValue}${stringMatch[QUOTE_GROUP_INDEX]}`
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
const numberPattern = /\b([2-9]\d*|0\d+)\b/g;
|
|
1968
|
+
let numberMatch = null;
|
|
1969
|
+
while ((numberMatch = numberPattern.exec(xpath)) !== null) {
|
|
1970
|
+
if (letStart >= ZERO_COUNT2 && numberMatch.index >= letStart && numberMatch.index < letEnd) {
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
const numberValue = numberMatch[NUMBER_VALUE_GROUP_INDEX];
|
|
1974
|
+
values.push({
|
|
1975
|
+
position: numberMatch.index,
|
|
1976
|
+
type: "number",
|
|
1977
|
+
value: numberValue
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
return values;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/tester/quality/checkQualityChecks.ts
|
|
1984
|
+
var MAX_MESSAGE_LENGTH = 80;
|
|
1985
|
+
var MIN_STRING_LENGTH7 = 0;
|
|
1986
|
+
var LINE_NUMBER_OFFSET2 = 1;
|
|
1987
|
+
var ARRAY_LAST_INDEX_OFFSET = 1;
|
|
1988
|
+
var NOT_FOUND_INDEX3 = -1;
|
|
1989
|
+
var ZERO_COUNT3 = 0;
|
|
1990
|
+
var SINGLE_OCCURRENCE = 1;
|
|
1991
|
+
var VERSION_PATTERN = /^Version:\s*(\d+)\.(\d+)\.(\d+)$/;
|
|
1992
|
+
var VARIABLE_DOC_PATTERN = /^(\$[a-zA-Z_][a-zA-Z0-9_]*):\s*.+$/;
|
|
1993
|
+
var INLINE_VIOLATION_MARKER = "Inline violation marker // \u274C";
|
|
1994
|
+
var INLINE_VALID_MARKER = "Inline valid marker // \u2705";
|
|
1995
|
+
var NEXT_LINE_OFFSET = 1;
|
|
1996
|
+
var VAR_PREFIX_LENGTH = 1;
|
|
1997
|
+
function hasAtLeastTwoItems(items) {
|
|
1998
|
+
return items.length > SINGLE_OCCURRENCE;
|
|
1999
|
+
}
|
|
2000
|
+
function findMessageLineNumber(xmlContent) {
|
|
2001
|
+
const lines = xmlContent.split("\n");
|
|
2002
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2003
|
+
const line = lines[i];
|
|
2004
|
+
if (line.includes("message=")) {
|
|
2005
|
+
return i + LINE_NUMBER_OFFSET2;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return void 0;
|
|
2009
|
+
}
|
|
2010
|
+
function findDescriptionLineNumber(xmlContent) {
|
|
2011
|
+
const lines = xmlContent.split("\n");
|
|
2012
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2013
|
+
const line = lines[i];
|
|
2014
|
+
if (line.includes("<description>")) {
|
|
2015
|
+
return i + LINE_NUMBER_OFFSET2;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
return void 0;
|
|
2019
|
+
}
|
|
2020
|
+
function findXPathValueLocation(xmlContent) {
|
|
2021
|
+
const lines = xmlContent.split("\n");
|
|
2022
|
+
let inProperties = false;
|
|
2023
|
+
let inXPathProperty = false;
|
|
2024
|
+
let valueStartLine = -1;
|
|
2025
|
+
let valueStartChar = -1;
|
|
2026
|
+
let valueContent = "";
|
|
2027
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2028
|
+
const line = lines[i];
|
|
2029
|
+
if (line.includes("<properties>")) {
|
|
2030
|
+
inProperties = true;
|
|
2031
|
+
}
|
|
2032
|
+
if (inProperties && line.includes('name="xpath"')) {
|
|
2033
|
+
inXPathProperty = true;
|
|
2034
|
+
}
|
|
2035
|
+
if (inXPathProperty) {
|
|
2036
|
+
const valueTagIndex = line.indexOf("<value>");
|
|
2037
|
+
if (valueTagIndex !== NOT_FOUND_INDEX3) {
|
|
2038
|
+
valueStartLine = i;
|
|
2039
|
+
valueStartChar = valueTagIndex + "<value>".length;
|
|
2040
|
+
let valueEndIndex = line.indexOf("</value>");
|
|
2041
|
+
if (valueEndIndex !== NOT_FOUND_INDEX3) {
|
|
2042
|
+
valueContent = line.substring(
|
|
2043
|
+
valueStartChar,
|
|
2044
|
+
valueEndIndex
|
|
2045
|
+
);
|
|
2046
|
+
} else {
|
|
2047
|
+
valueContent = line.substring(valueStartChar);
|
|
2048
|
+
for (let j = i + NEXT_LINE_OFFSET; j < lines.length; j++) {
|
|
2049
|
+
const nextLine = lines[j];
|
|
2050
|
+
const endTagIndex = nextLine.indexOf("</value>");
|
|
2051
|
+
if (endTagIndex !== NOT_FOUND_INDEX3) {
|
|
2052
|
+
valueContent += "\n" + nextLine.substring(ZERO_COUNT3, endTagIndex);
|
|
2053
|
+
break;
|
|
2054
|
+
}
|
|
2055
|
+
valueContent += "\n" + nextLine;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
const trimmedContent = valueContent.trim();
|
|
2059
|
+
let actualContentStartLine = valueStartLine;
|
|
2060
|
+
let actualContent = trimmedContent;
|
|
2061
|
+
const cdataIndexInValue = valueContent.indexOf("<![CDATA[");
|
|
2062
|
+
if (cdataIndexInValue !== NOT_FOUND_INDEX3) {
|
|
2063
|
+
const afterCdataStart = valueContent.substring(
|
|
2064
|
+
cdataIndexInValue + "<![CDATA[".length
|
|
2065
|
+
);
|
|
2066
|
+
const cdataEndIndex = afterCdataStart.indexOf("]]>");
|
|
2067
|
+
if (cdataEndIndex !== NOT_FOUND_INDEX3) {
|
|
2068
|
+
actualContent = afterCdataStart.substring(ZERO_COUNT3, cdataEndIndex).trim();
|
|
2069
|
+
const newlineCountToCdata = (valueContent.substring(ZERO_COUNT3, cdataIndexInValue).match(/\n/g) ?? []).length;
|
|
2070
|
+
actualContentStartLine = valueStartLine + newlineCountToCdata + LINE_NUMBER_OFFSET2;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
return {
|
|
2074
|
+
startChar: valueStartChar,
|
|
2075
|
+
startLine: actualContentStartLine + LINE_NUMBER_OFFSET2,
|
|
2076
|
+
valueContent: actualContent.trim()
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
if (line.includes("</properties>")) {
|
|
2081
|
+
break;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
return void 0;
|
|
2085
|
+
}
|
|
2086
|
+
function calculateXPathLineNumber(xpathValueLocation, positionInXPath) {
|
|
2087
|
+
const xpathBeforePosition = xpathValueLocation.valueContent.substring(
|
|
2088
|
+
ZERO_COUNT3,
|
|
2089
|
+
positionInXPath
|
|
2090
|
+
);
|
|
2091
|
+
const newlineCount = (xpathBeforePosition.match(/\n/g) ?? []).length;
|
|
2092
|
+
return xpathValueLocation.startLine + newlineCount;
|
|
2093
|
+
}
|
|
2094
|
+
function checkMessageLength(message, messageLine) {
|
|
2095
|
+
if (message === null || message === void 0 || message.length === MIN_STRING_LENGTH7) {
|
|
2096
|
+
return messageLine !== void 0 ? `Line ${String(messageLine)}: message attribute is missing` : "message attribute is missing";
|
|
2097
|
+
}
|
|
2098
|
+
if (message.length > MAX_MESSAGE_LENGTH) {
|
|
2099
|
+
return messageLine !== void 0 ? `Line ${String(messageLine)}: message attribute exceeds ${String(MAX_MESSAGE_LENGTH)} characters (${String(message.length)} chars)` : `message attribute exceeds ${String(MAX_MESSAGE_LENGTH)} characters (${String(message.length)} chars)`;
|
|
2100
|
+
}
|
|
2101
|
+
return null;
|
|
2102
|
+
}
|
|
2103
|
+
function checkVersionLine(description, descriptionLine) {
|
|
2104
|
+
if (description === null || description === void 0 || description.length === MIN_STRING_LENGTH7) {
|
|
2105
|
+
return descriptionLine !== void 0 ? `Line ${String(descriptionLine)}: description must end with 'Version: X.Y.Z'` : "description must end with 'Version: X.Y.Z'";
|
|
2106
|
+
}
|
|
2107
|
+
const lines = description.split("\n");
|
|
2108
|
+
const lastLineIndex = lines.length - ARRAY_LAST_INDEX_OFFSET;
|
|
2109
|
+
const lastLine = lines[lastLineIndex]?.trim();
|
|
2110
|
+
if (lastLine === void 0 || lastLine.length === MIN_STRING_LENGTH7 || !VERSION_PATTERN.test(lastLine)) {
|
|
2111
|
+
return descriptionLine !== void 0 ? `Line ${String(descriptionLine)}: description must end with 'Version: X.Y.Z' (SemVer format)` : "description must end with 'Version: X.Y.Z' (SemVer format)";
|
|
2112
|
+
}
|
|
2113
|
+
return null;
|
|
2114
|
+
}
|
|
2115
|
+
function checkXPathHardcodedValues(xpath, xmlContent) {
|
|
2116
|
+
const issues = [];
|
|
2117
|
+
if (xpath === null || xpath === void 0 || xpath.length === MIN_STRING_LENGTH7) {
|
|
2118
|
+
return issues;
|
|
2119
|
+
}
|
|
2120
|
+
const xpathLocation = findXPathValueLocation(xmlContent);
|
|
2121
|
+
if (xpathLocation === void 0) {
|
|
2122
|
+
return issues;
|
|
2123
|
+
}
|
|
2124
|
+
const rawXPath = xpathLocation.valueContent;
|
|
2125
|
+
const hardcodedValues = extractHardcodedValues(rawXPath);
|
|
2126
|
+
if (hardcodedValues.length === MIN_STRING_LENGTH7) {
|
|
2127
|
+
return issues;
|
|
2128
|
+
}
|
|
2129
|
+
const letVariables = extractLetVariables(xpath);
|
|
2130
|
+
const letVariableValues = new Set(
|
|
2131
|
+
letVariables.map((v) => {
|
|
2132
|
+
const normalized = v.value.replace(/^['"]|['"]$/g, "").toLowerCase().trim();
|
|
2133
|
+
return normalized;
|
|
2134
|
+
})
|
|
2135
|
+
);
|
|
2136
|
+
for (const hardcoded of hardcodedValues) {
|
|
2137
|
+
const normalizedValue = hardcoded.value.replace(/^['"]|['"]$/g, "").toLowerCase().trim();
|
|
2138
|
+
if (!letVariableValues.has(normalizedValue)) {
|
|
2139
|
+
const exactLine = calculateXPathLineNumber(
|
|
2140
|
+
xpathLocation,
|
|
2141
|
+
hardcoded.position
|
|
2142
|
+
);
|
|
2143
|
+
issues.push(
|
|
2144
|
+
`Line ${String(exactLine)}: ${hardcoded.value} outside initial let statement`
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return issues;
|
|
2149
|
+
}
|
|
2150
|
+
function findVariablePositionInXPath(xpath, varName) {
|
|
2151
|
+
const pattern = new RegExp(
|
|
2152
|
+
`\\$${varName.substring(VAR_PREFIX_LENGTH)}\\s*:?=`
|
|
2153
|
+
);
|
|
2154
|
+
const match = pattern.exec(xpath);
|
|
2155
|
+
return match?.index ?? NOT_FOUND_INDEX3;
|
|
2156
|
+
}
|
|
2157
|
+
function checkVariableDocumentation(xpath, description, xmlContent) {
|
|
2158
|
+
const issues = [];
|
|
2159
|
+
if (xpath === null || xpath === void 0 || xpath.length === MIN_STRING_LENGTH7) {
|
|
2160
|
+
return issues;
|
|
2161
|
+
}
|
|
2162
|
+
const letVariables = extractLetVariables(xpath);
|
|
2163
|
+
if (letVariables.length === MIN_STRING_LENGTH7) {
|
|
2164
|
+
return issues;
|
|
2165
|
+
}
|
|
2166
|
+
if (description === null || description === void 0 || description.length === MIN_STRING_LENGTH7) {
|
|
2167
|
+
const xpathLocation2 = findXPathValueLocation(xmlContent);
|
|
2168
|
+
for (const variable of letVariables) {
|
|
2169
|
+
const xpathForPosition = xpathLocation2 !== void 0 ? xpathLocation2.valueContent : xpath;
|
|
2170
|
+
const varPosition = findVariablePositionInXPath(
|
|
2171
|
+
xpathForPosition,
|
|
2172
|
+
variable.name
|
|
2173
|
+
);
|
|
2174
|
+
const varLine = xpathLocation2 !== void 0 && varPosition !== NOT_FOUND_INDEX3 ? calculateXPathLineNumber(xpathLocation2, varPosition) : void 0;
|
|
2175
|
+
issues.push(
|
|
2176
|
+
varLine !== void 0 ? `Line ${String(varLine)}: variable ${variable.name} undocumented` : `variable ${variable.name} undocumented`
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
return issues;
|
|
2180
|
+
}
|
|
2181
|
+
const descriptionLines = description.split("\n");
|
|
2182
|
+
const documentedVars = /* @__PURE__ */ new Set();
|
|
2183
|
+
for (const line of descriptionLines) {
|
|
2184
|
+
const trimmed = line.trim();
|
|
2185
|
+
const varMatch = VARIABLE_DOC_PATTERN.exec(trimmed);
|
|
2186
|
+
if (varMatch !== null) {
|
|
2187
|
+
const colonIndex = trimmed.indexOf(":");
|
|
2188
|
+
const varName = trimmed.substring(ZERO_COUNT3, colonIndex);
|
|
2189
|
+
documentedVars.add(varName);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
const xpathLocation = findXPathValueLocation(xmlContent);
|
|
2193
|
+
for (const variable of letVariables) {
|
|
2194
|
+
if (!documentedVars.has(variable.name)) {
|
|
2195
|
+
const xpathForPosition = xpathLocation !== void 0 ? xpathLocation.valueContent : xpath;
|
|
2196
|
+
const varPosition = findVariablePositionInXPath(
|
|
2197
|
+
xpathForPosition,
|
|
2198
|
+
variable.name
|
|
2199
|
+
);
|
|
2200
|
+
const varLine = xpathLocation !== void 0 && varPosition !== NOT_FOUND_INDEX3 ? calculateXPathLineNumber(xpathLocation, varPosition) : void 0;
|
|
2201
|
+
issues.push(
|
|
2202
|
+
varLine !== void 0 ? `Line ${String(varLine)}: variable ${variable.name} undocumented` : `variable ${variable.name} undocumented`
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return issues;
|
|
2207
|
+
}
|
|
2208
|
+
function extractTextAfterMarker(line, marker) {
|
|
2209
|
+
const markerIndex = line.indexOf(marker);
|
|
2210
|
+
const textAfter = line.substring(markerIndex + marker.length).trim();
|
|
2211
|
+
return textAfter;
|
|
2212
|
+
}
|
|
2213
|
+
function findMarkerLineNumber(input) {
|
|
2214
|
+
const { exampleIndex, lines, markerLineInExample } = input;
|
|
2215
|
+
let currentExampleIndex = 0;
|
|
2216
|
+
let exampleStart = NOT_FOUND_INDEX3;
|
|
2217
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2218
|
+
const line = lines[i];
|
|
2219
|
+
if (line.includes("<example>")) {
|
|
2220
|
+
currentExampleIndex++;
|
|
2221
|
+
if (currentExampleIndex === exampleIndex) {
|
|
2222
|
+
exampleStart = i;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
if (exampleStart >= ZERO_COUNT3 && line.includes("</example>")) {
|
|
2226
|
+
const lineOffset = markerLineInExample - LINE_NUMBER_OFFSET2;
|
|
2227
|
+
const estimatedLine = exampleStart + lineOffset + LINE_NUMBER_OFFSET2;
|
|
2228
|
+
if (estimatedLine < i) {
|
|
2229
|
+
return estimatedLine + LINE_NUMBER_OFFSET2;
|
|
2230
|
+
}
|
|
2231
|
+
break;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
return void 0;
|
|
2235
|
+
}
|
|
2236
|
+
function checkMarkerDescriptions(examples, xmlContent) {
|
|
2237
|
+
const issues = [];
|
|
2238
|
+
const lines = xmlContent.split("\n");
|
|
2239
|
+
const markerDescriptions = /* @__PURE__ */ new Map();
|
|
2240
|
+
for (const example of examples) {
|
|
2241
|
+
const exampleLines = example.content.split("\n");
|
|
2242
|
+
for (const marker of example.violationMarkers) {
|
|
2243
|
+
const markerLineIndex = marker.lineNumber - LINE_NUMBER_OFFSET2;
|
|
2244
|
+
if (markerLineIndex >= ZERO_COUNT3 && markerLineIndex < exampleLines.length) {
|
|
2245
|
+
const markerLine = exampleLines[markerLineIndex];
|
|
2246
|
+
let desc = "";
|
|
2247
|
+
if (markerLine.includes("// \u274C")) {
|
|
2248
|
+
desc = extractTextAfterMarker(markerLine, "// \u274C").trim();
|
|
2249
|
+
} else if (markerLine.includes("// Violation:")) {
|
|
2250
|
+
desc = extractTextAfterMarker(
|
|
2251
|
+
markerLine,
|
|
2252
|
+
"// Violation:"
|
|
2253
|
+
).trim();
|
|
2254
|
+
} else {
|
|
2255
|
+
desc = marker.description.trim();
|
|
2256
|
+
if (desc === INLINE_VIOLATION_MARKER) {
|
|
2257
|
+
desc = "";
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
if (desc.length === MIN_STRING_LENGTH7) {
|
|
2261
|
+
const lineNum = findMarkerLineNumber({
|
|
2262
|
+
exampleIndex: example.exampleIndex,
|
|
2263
|
+
lines,
|
|
2264
|
+
markerLineInExample: marker.lineNumber
|
|
2265
|
+
});
|
|
2266
|
+
issues.push(
|
|
2267
|
+
lineNum !== void 0 ? `Line ${String(lineNum)}: violation has no description` : `Example ${String(example.exampleIndex)}: violation has no description`
|
|
2268
|
+
);
|
|
2269
|
+
} else {
|
|
2270
|
+
if (!markerDescriptions.has(desc)) {
|
|
2271
|
+
markerDescriptions.set(desc, []);
|
|
2272
|
+
}
|
|
2273
|
+
const locations = markerDescriptions.get(desc);
|
|
2274
|
+
const lineNum = findMarkerLineNumber({
|
|
2275
|
+
exampleIndex: example.exampleIndex,
|
|
2276
|
+
lines,
|
|
2277
|
+
markerLineInExample: marker.lineNumber
|
|
2278
|
+
});
|
|
2279
|
+
locations.push({
|
|
2280
|
+
example: example.exampleIndex,
|
|
2281
|
+
line: lineNum ?? ZERO_COUNT3
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
for (const marker of example.validMarkers) {
|
|
2287
|
+
const markerLineIndex = marker.lineNumber - LINE_NUMBER_OFFSET2;
|
|
2288
|
+
if (markerLineIndex >= ZERO_COUNT3 && markerLineIndex < exampleLines.length) {
|
|
2289
|
+
const markerLine = exampleLines[markerLineIndex];
|
|
2290
|
+
let desc = "";
|
|
2291
|
+
if (markerLine.includes("// \u2705")) {
|
|
2292
|
+
desc = extractTextAfterMarker(markerLine, "// \u2705").trim();
|
|
2293
|
+
} else if (markerLine.includes("// Valid:")) {
|
|
2294
|
+
desc = extractTextAfterMarker(
|
|
2295
|
+
markerLine,
|
|
2296
|
+
"// Valid:"
|
|
2297
|
+
).trim();
|
|
2298
|
+
} else {
|
|
2299
|
+
desc = marker.description.trim();
|
|
2300
|
+
if (desc === INLINE_VALID_MARKER) {
|
|
2301
|
+
desc = "";
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
if (desc.length === MIN_STRING_LENGTH7) {
|
|
2305
|
+
const lineNum = findMarkerLineNumber({
|
|
2306
|
+
exampleIndex: example.exampleIndex,
|
|
2307
|
+
lines,
|
|
2308
|
+
markerLineInExample: marker.lineNumber
|
|
2309
|
+
});
|
|
2310
|
+
issues.push(
|
|
2311
|
+
lineNum !== void 0 ? `Line ${String(lineNum)}: valid has no description` : `Example ${String(example.exampleIndex)}: valid has no description`
|
|
2312
|
+
);
|
|
2313
|
+
} else {
|
|
2314
|
+
if (!markerDescriptions.has(desc)) {
|
|
2315
|
+
markerDescriptions.set(desc, []);
|
|
2316
|
+
}
|
|
2317
|
+
const locations = markerDescriptions.get(desc);
|
|
2318
|
+
const lineNum = findMarkerLineNumber({
|
|
2319
|
+
exampleIndex: example.exampleIndex,
|
|
2320
|
+
lines,
|
|
2321
|
+
markerLineInExample: marker.lineNumber
|
|
2322
|
+
});
|
|
2323
|
+
locations.push({
|
|
2324
|
+
example: example.exampleIndex,
|
|
2325
|
+
line: lineNum ?? ZERO_COUNT3
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
for (const [desc, locations] of markerDescriptions.entries()) {
|
|
2332
|
+
if (hasAtLeastTwoItems(locations)) {
|
|
2333
|
+
const [firstLocation, ...restLocations] = locations;
|
|
2334
|
+
const originalLine = firstLocation.line;
|
|
2335
|
+
for (const loc of restLocations) {
|
|
2336
|
+
issues.push(
|
|
2337
|
+
`Line ${String(loc.line)}: duplicate description '${desc}' (line ${String(originalLine)})`
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return issues;
|
|
2343
|
+
}
|
|
2344
|
+
function checkViolationExists(examples, xmlContent) {
|
|
2345
|
+
const hasViolation = examples.some(
|
|
2346
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameter
|
|
2347
|
+
(example) => example.violationMarkers.length > MIN_STRING_LENGTH7
|
|
2348
|
+
);
|
|
2349
|
+
if (!hasViolation) {
|
|
2350
|
+
const lines = xmlContent.split("\n");
|
|
2351
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2352
|
+
const line = lines[i];
|
|
2353
|
+
if (line.includes("<example>")) {
|
|
2354
|
+
return `Line ${String(i + LINE_NUMBER_OFFSET2)}: at least one violation marker (// Violation or // \u274C) required`;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
return "at least one violation marker (// Violation or // \u274C) required";
|
|
2358
|
+
}
|
|
2359
|
+
return null;
|
|
2360
|
+
}
|
|
2361
|
+
function checkQualityChecks(ruleFilePath, ruleMetadata, examples) {
|
|
2362
|
+
const issues = [];
|
|
2363
|
+
try {
|
|
2364
|
+
const xmlContent = readFileSync3(ruleFilePath, "utf-8");
|
|
2365
|
+
const messageLine = findMessageLineNumber(xmlContent);
|
|
2366
|
+
const messageIssue = checkMessageLength(
|
|
2367
|
+
ruleMetadata.message,
|
|
2368
|
+
messageLine
|
|
2369
|
+
);
|
|
2370
|
+
if (messageIssue !== null) {
|
|
2371
|
+
issues.push(messageIssue);
|
|
2372
|
+
}
|
|
2373
|
+
const descriptionLine = findDescriptionLineNumber(xmlContent);
|
|
2374
|
+
const versionIssue = checkVersionLine(
|
|
2375
|
+
ruleMetadata.description,
|
|
2376
|
+
descriptionLine
|
|
2377
|
+
);
|
|
2378
|
+
if (versionIssue !== null) {
|
|
2379
|
+
issues.push(versionIssue);
|
|
2380
|
+
}
|
|
2381
|
+
const xpathIssues = checkXPathHardcodedValues(
|
|
2382
|
+
ruleMetadata.xpath,
|
|
2383
|
+
xmlContent
|
|
2384
|
+
);
|
|
2385
|
+
issues.push(...xpathIssues);
|
|
2386
|
+
const varDocIssues = checkVariableDocumentation(
|
|
2387
|
+
ruleMetadata.xpath,
|
|
2388
|
+
ruleMetadata.description,
|
|
2389
|
+
xmlContent
|
|
2390
|
+
);
|
|
2391
|
+
issues.push(...varDocIssues);
|
|
2392
|
+
const markerIssues = checkMarkerDescriptions(examples, xmlContent);
|
|
2393
|
+
issues.push(...markerIssues);
|
|
2394
|
+
const violationIssue = checkViolationExists(examples, xmlContent);
|
|
2395
|
+
if (violationIssue !== null) {
|
|
2396
|
+
issues.push(violationIssue);
|
|
2397
|
+
}
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2400
|
+
issues.push(`Error reading rule file: ${errorMessage}`);
|
|
2401
|
+
}
|
|
2402
|
+
const MIN_ISSUES_COUNT2 = 0;
|
|
2403
|
+
return {
|
|
2404
|
+
issues,
|
|
2405
|
+
passed: issues.length === MIN_ISSUES_COUNT2,
|
|
2406
|
+
warnings: []
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
|
|
1756
2410
|
// src/tester/RuleTester.ts
|
|
1757
2411
|
var MIN_EXAMPLES_COUNT2 = 0;
|
|
1758
2412
|
var MIN_VIOLATIONS_COUNT = 0;
|
|
@@ -1808,7 +2462,7 @@ var RuleTester = class {
|
|
|
1808
2462
|
* @public
|
|
1809
2463
|
*/
|
|
1810
2464
|
extractRuleMetadata() {
|
|
1811
|
-
const content =
|
|
2465
|
+
const content = readFileSync4(this.ruleFilePath, "utf-8");
|
|
1812
2466
|
const parser = new DOMParser3();
|
|
1813
2467
|
const doc = parser.parseFromString(content, "text/xml");
|
|
1814
2468
|
const ruleElement = doc.getElementsByTagName("rule")[MIN_EXAMPLES_COUNT2];
|
|
@@ -1847,7 +2501,7 @@ var RuleTester = class {
|
|
|
1847
2501
|
* @public
|
|
1848
2502
|
*/
|
|
1849
2503
|
extractExamples() {
|
|
1850
|
-
const content =
|
|
2504
|
+
const content = readFileSync4(this.ruleFilePath, "utf-8");
|
|
1851
2505
|
const parser = new DOMParser3();
|
|
1852
2506
|
const doc = parser.parseFromString(content, "text/xml");
|
|
1853
2507
|
const exampleNodes = doc.getElementsByTagName("example");
|
|
@@ -1906,13 +2560,9 @@ var RuleTester = class {
|
|
|
1906
2560
|
*/
|
|
1907
2561
|
async runCoverageTest(skipPMDValidation = false, maxConcurrency = DEFAULT_CONCURRENCY) {
|
|
1908
2562
|
this.extractExamples();
|
|
1909
|
-
const qualityResult = runQualityChecks(
|
|
1910
|
-
this.ruleMetadata,
|
|
1911
|
-
this.examples
|
|
1912
|
-
);
|
|
1913
2563
|
const INDEX_OFFSET3 = 1;
|
|
1914
2564
|
const ZERO_VIOLATIONS = 0;
|
|
1915
|
-
const
|
|
2565
|
+
const pmdValidationPromise = skipPMDValidation ? Promise.resolve(
|
|
1916
2566
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameter for map
|
|
1917
2567
|
this.examples.map((_example, i) => ({
|
|
1918
2568
|
actualViolations: ZERO_VIOLATIONS,
|
|
@@ -1922,7 +2572,34 @@ var RuleTester = class {
|
|
|
1922
2572
|
passed: true,
|
|
1923
2573
|
testCaseResults: []
|
|
1924
2574
|
}))
|
|
1925
|
-
) :
|
|
2575
|
+
) : this.validateExamplesWithPMD(maxConcurrency);
|
|
2576
|
+
const qualityChecksPromise = Promise.resolve(
|
|
2577
|
+
runQualityChecks(
|
|
2578
|
+
this.ruleFilePath,
|
|
2579
|
+
this.ruleMetadata,
|
|
2580
|
+
this.examples
|
|
2581
|
+
)
|
|
2582
|
+
);
|
|
2583
|
+
const newQualityChecksPromise = Promise.resolve(
|
|
2584
|
+
checkQualityChecks(
|
|
2585
|
+
this.ruleFilePath,
|
|
2586
|
+
this.ruleMetadata,
|
|
2587
|
+
this.examples
|
|
2588
|
+
)
|
|
2589
|
+
);
|
|
2590
|
+
const xpathCoveragePromise = Promise.resolve(
|
|
2591
|
+
checkXPathCoverage(
|
|
2592
|
+
this.ruleMetadata.xpath,
|
|
2593
|
+
this.examples,
|
|
2594
|
+
this.ruleFilePath
|
|
2595
|
+
)
|
|
2596
|
+
);
|
|
2597
|
+
const [exampleResults, qualityResult, newQualityChecks, xpathCoverage] = await Promise.all([
|
|
2598
|
+
pmdValidationPromise,
|
|
2599
|
+
qualityChecksPromise,
|
|
2600
|
+
newQualityChecksPromise,
|
|
2601
|
+
xpathCoveragePromise
|
|
2602
|
+
]);
|
|
1926
2603
|
this.results.examplesTested = this.examples.length;
|
|
1927
2604
|
this.results.examplesPassed = exampleResults.filter(
|
|
1928
2605
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameter
|
|
@@ -1941,12 +2618,8 @@ var RuleTester = class {
|
|
|
1941
2618
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- Callback parameter
|
|
1942
2619
|
(result) => result.testCaseResults
|
|
1943
2620
|
);
|
|
1944
|
-
const xpathCoverage = checkXPathCoverage(
|
|
1945
|
-
this.ruleMetadata.xpath,
|
|
1946
|
-
this.examples,
|
|
1947
|
-
this.ruleFilePath
|
|
1948
|
-
);
|
|
1949
2621
|
this.results.xpathCoverage = xpathCoverage;
|
|
2622
|
+
this.results.qualityChecks = newQualityChecks;
|
|
1950
2623
|
this.results.success = qualityResult.passed && this.examples.length > MIN_EXAMPLES_COUNT2 && this.results.examplesPassed === this.results.examplesTested;
|
|
1951
2624
|
return Promise.resolve(this.results);
|
|
1952
2625
|
}
|
|
@@ -2103,11 +2776,11 @@ var RuleTester = class {
|
|
|
2103
2776
|
*/
|
|
2104
2777
|
findTestCaseLineNumber(exampleIndex, testType) {
|
|
2105
2778
|
try {
|
|
2106
|
-
const content =
|
|
2779
|
+
const content = readFileSync4(this.ruleFilePath, "utf-8");
|
|
2107
2780
|
const lines = content.split("\n");
|
|
2108
|
-
const
|
|
2109
|
-
let exampleStart =
|
|
2110
|
-
let exampleEnd =
|
|
2781
|
+
const NOT_FOUND_INDEX4 = -1;
|
|
2782
|
+
let exampleStart = NOT_FOUND_INDEX4;
|
|
2783
|
+
let exampleEnd = NOT_FOUND_INDEX4;
|
|
2111
2784
|
let currentExampleIndex = 0;
|
|
2112
2785
|
for (let i = 0; i < lines.length; i++) {
|
|
2113
2786
|
const line = lines[i];
|
|
@@ -2119,7 +2792,7 @@ var RuleTester = class {
|
|
|
2119
2792
|
break;
|
|
2120
2793
|
}
|
|
2121
2794
|
}
|
|
2122
|
-
if (exampleStart ===
|
|
2795
|
+
if (exampleStart === NOT_FOUND_INDEX4 || exampleEnd === NOT_FOUND_INDEX4) {
|
|
2123
2796
|
return void 0;
|
|
2124
2797
|
}
|
|
2125
2798
|
const hasInlineMarkers = () => {
|
|
@@ -2131,24 +2804,24 @@ var RuleTester = class {
|
|
|
2131
2804
|
}
|
|
2132
2805
|
return false;
|
|
2133
2806
|
};
|
|
2134
|
-
const
|
|
2807
|
+
const LINE_NUMBER_OFFSET3 = 1;
|
|
2135
2808
|
const hasInline = hasInlineMarkers();
|
|
2136
2809
|
for (let i = exampleStart; i <= exampleEnd; i++) {
|
|
2137
2810
|
const line = lines[i];
|
|
2138
2811
|
if (hasInline) {
|
|
2139
2812
|
const inlineMarkerText = testType === "violation" ? "// \u274C" : "// \u2705";
|
|
2140
2813
|
if (line.includes(inlineMarkerText)) {
|
|
2141
|
-
return i +
|
|
2814
|
+
return i + LINE_NUMBER_OFFSET3;
|
|
2142
2815
|
}
|
|
2143
2816
|
} else {
|
|
2144
2817
|
const sectionMarkerText = testType === "violation" ? "// Violation:" : "// Valid:";
|
|
2145
2818
|
if (line.includes(sectionMarkerText)) {
|
|
2146
|
-
const
|
|
2147
|
-
for (let j = i +
|
|
2819
|
+
const NEXT_LINE_OFFSET2 = 1;
|
|
2820
|
+
for (let j = i + NEXT_LINE_OFFSET2; j <= exampleEnd; j++) {
|
|
2148
2821
|
const nextLineRaw = lines[j];
|
|
2149
2822
|
const nextLine = nextLineRaw.trim();
|
|
2150
2823
|
if (nextLine && !nextLine.startsWith("//") && !nextLine.startsWith("*/") && !nextLine.startsWith("/*") && !nextLine.startsWith("</") && !nextLine.startsWith("<")) {
|
|
2151
|
-
return j +
|
|
2824
|
+
return j + LINE_NUMBER_OFFSET3;
|
|
2152
2825
|
}
|
|
2153
2826
|
}
|
|
2154
2827
|
}
|
|
@@ -2165,11 +2838,11 @@ var RuleTester = class {
|
|
|
2165
2838
|
* @private
|
|
2166
2839
|
*/
|
|
2167
2840
|
findExampleLineNumber(exampleIndex) {
|
|
2168
|
-
const content =
|
|
2841
|
+
const content = readFileSync4(this.ruleFilePath, "utf-8");
|
|
2169
2842
|
const lines = content.split("\n");
|
|
2170
2843
|
let currentExampleIndex = 0;
|
|
2171
|
-
const
|
|
2172
|
-
const
|
|
2844
|
+
const LINE_NUMBER_OFFSET3 = 1;
|
|
2845
|
+
const NOT_FOUND_INDEX4 = -1;
|
|
2173
2846
|
const foundIndex = lines.findIndex((line) => {
|
|
2174
2847
|
if (line.includes("<example>")) {
|
|
2175
2848
|
currentExampleIndex++;
|
|
@@ -2177,7 +2850,7 @@ var RuleTester = class {
|
|
|
2177
2850
|
}
|
|
2178
2851
|
return false;
|
|
2179
2852
|
});
|
|
2180
|
-
return foundIndex ===
|
|
2853
|
+
return foundIndex === NOT_FOUND_INDEX4 ? void 0 : foundIndex + LINE_NUMBER_OFFSET3;
|
|
2181
2854
|
}
|
|
2182
2855
|
/**
|
|
2183
2856
|
* Initializes an empty results object for a new test run.
|
|
@@ -2295,6 +2968,8 @@ function generateLcovReport(coverageData, outputPath) {
|
|
|
2295
2968
|
// src/cli/main.ts
|
|
2296
2969
|
var EXIT_CODE_SUCCESS = 0;
|
|
2297
2970
|
var EXIT_CODE_ERROR = 1;
|
|
2971
|
+
var PARSE_INT_RADIX = 10;
|
|
2972
|
+
var LINE_NUMBER_MATCH_GROUP_INDEX = 1;
|
|
2298
2973
|
var ARGV_SLICE_INDEX = 2;
|
|
2299
2974
|
var MIN_ARGS_COUNT = 0;
|
|
2300
2975
|
var MAX_ARGS_COUNT = 2;
|
|
@@ -2320,11 +2995,8 @@ async function testRuleFile(ruleFilePath, coverageTracker, maxConcurrency) {
|
|
|
2320
2995
|
try {
|
|
2321
2996
|
const tester = new RuleTester(ruleFilePath);
|
|
2322
2997
|
const result = await tester.runCoverageTest(false, maxConcurrency);
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
const coveredLines = result.xpathCoverage.coveredLineNumbers;
|
|
2326
|
-
if (hasCoverageTracker && coveredLines && coveredLines.length > MIN_COVERED_LINES_COUNT) {
|
|
2327
|
-
for (const lineNumber of coveredLines) {
|
|
2998
|
+
if (coverageTracker && result.xpathCoverage.coveredLineNumbers) {
|
|
2999
|
+
for (const lineNumber of result.xpathCoverage.coveredLineNumbers) {
|
|
2328
3000
|
coverageTracker.recordXPathLine(lineNumber);
|
|
2329
3001
|
}
|
|
2330
3002
|
}
|
|
@@ -2358,12 +3030,42 @@ async function testRuleFile(ruleFilePath, coverageTracker, maxConcurrency) {
|
|
|
2358
3030
|
` Rule triggers violations: ${result.ruleTriggersViolations ? "\u2705 Yes" : "\u274C No"}`
|
|
2359
3031
|
);
|
|
2360
3032
|
}
|
|
3033
|
+
if (result.qualityChecks) {
|
|
3034
|
+
console.log("\n\u2B50 Quality Checks:");
|
|
3035
|
+
if (result.qualityChecks.passed) {
|
|
3036
|
+
console.log(" Status: \u2705 Passed");
|
|
3037
|
+
} else {
|
|
3038
|
+
console.log(" Status: \u26A0\uFE0F Incomplete");
|
|
3039
|
+
const sortedIssues = [...result.qualityChecks.issues].sort(
|
|
3040
|
+
(a, b) => {
|
|
3041
|
+
const lineMatchA = /^Line\s+(\d+):/.exec(a);
|
|
3042
|
+
const lineMatchB = /^Line\s+(\d+):/.exec(b);
|
|
3043
|
+
const lineNumberA = lineMatchA?.[LINE_NUMBER_MATCH_GROUP_INDEX];
|
|
3044
|
+
const lineNumberB = lineMatchB?.[LINE_NUMBER_MATCH_GROUP_INDEX];
|
|
3045
|
+
const lineNumA = lineMatchA ? Number.parseInt(
|
|
3046
|
+
lineNumberA ?? "",
|
|
3047
|
+
PARSE_INT_RADIX
|
|
3048
|
+
) : Number.MAX_SAFE_INTEGER;
|
|
3049
|
+
const lineNumB = lineMatchB ? Number.parseInt(
|
|
3050
|
+
lineNumberB ?? "",
|
|
3051
|
+
PARSE_INT_RADIX
|
|
3052
|
+
) : Number.MAX_SAFE_INTEGER;
|
|
3053
|
+
if (lineNumA !== lineNumB) {
|
|
3054
|
+
return lineNumA - lineNumB;
|
|
3055
|
+
}
|
|
3056
|
+
return a.localeCompare(b);
|
|
3057
|
+
}
|
|
3058
|
+
);
|
|
3059
|
+
for (const issue of sortedIssues) {
|
|
3060
|
+
console.log(` - ${issue}`);
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
2361
3064
|
console.log("\n\u{1F50D} XPath Coverage:");
|
|
2362
3065
|
if (result.xpathCoverage.overallSuccess) {
|
|
2363
3066
|
console.log(" Status: \u2705 Complete");
|
|
2364
3067
|
} else {
|
|
2365
|
-
|
|
2366
|
-
console.log(INCOMPLETE_STATUS_MESSAGE);
|
|
3068
|
+
console.log(" Status: \u26A0\uFE0F Incomplete");
|
|
2367
3069
|
}
|
|
2368
3070
|
if (result.xpathCoverage.coverage.length > MIN_COUNT4) {
|
|
2369
3071
|
console.log(
|
|
@@ -2425,7 +3127,7 @@ async function testRuleFile(ruleFilePath, coverageTracker, maxConcurrency) {
|
|
|
2425
3127
|
console.log("\n\u274C Tests failed, incomplete coverage");
|
|
2426
3128
|
}
|
|
2427
3129
|
tester.cleanup();
|
|
2428
|
-
const coverageData = coverageTracker
|
|
3130
|
+
const coverageData = coverageTracker ? coverageTracker.getCoverageData() : void 0;
|
|
2429
3131
|
return {
|
|
2430
3132
|
coverageData,
|
|
2431
3133
|
filePath: ruleFilePath,
|
|
@@ -2462,8 +3164,7 @@ async function main() {
|
|
|
2462
3164
|
console.error("\u274C Invalid path argument");
|
|
2463
3165
|
process.exit(EXIT_CODE_ERROR);
|
|
2464
3166
|
}
|
|
2465
|
-
const
|
|
2466
|
-
const hasCoverageFlag = args.length > SECOND_ARG_INDEX && args[SECOND_ARG_INDEX] === COVERAGE_FLAG;
|
|
3167
|
+
const hasCoverageFlag = args.length > SECOND_ARG_INDEX && args[SECOND_ARG_INDEX] === "--coverage";
|
|
2467
3168
|
if (!existsSync2(pathArg)) {
|
|
2468
3169
|
console.error(`\u274C Path not found: ${pathArg}`);
|
|
2469
3170
|
process.exit(EXIT_CODE_ERROR);
|
|
@@ -2501,8 +3202,8 @@ async function main() {
|
|
|
2501
3202
|
const coverageTrackers = hasCoverageFlag ? /* @__PURE__ */ new Map() : null;
|
|
2502
3203
|
const tasks = xmlFiles.map(
|
|
2503
3204
|
(filePath) => async () => {
|
|
2504
|
-
const tracker = coverageTrackers
|
|
2505
|
-
if (tracker
|
|
3205
|
+
const tracker = coverageTrackers ? coverageTrackers.get(filePath) ?? new CoverageTracker(filePath) : null;
|
|
3206
|
+
if (tracker && coverageTrackers) {
|
|
2506
3207
|
coverageTrackers.set(filePath, tracker);
|
|
2507
3208
|
}
|
|
2508
3209
|
return testRuleFile(filePath, tracker, maxExampleConcurrency);
|
|
@@ -2532,19 +3233,13 @@ async function main() {
|
|
|
2532
3233
|
console.log(` - ${result.filePath}${errorSuffix}`);
|
|
2533
3234
|
});
|
|
2534
3235
|
}
|
|
2535
|
-
if (hasCoverageFlag && coverageTrackers
|
|
2536
|
-
const coverageData = Array.from(
|
|
2537
|
-
coverageTrackers.values()
|
|
2538
|
-
).map(
|
|
3236
|
+
if (hasCoverageFlag && coverageTrackers) {
|
|
3237
|
+
const coverageData = Array.from(coverageTrackers.values()).map(
|
|
2539
3238
|
(tracker) => tracker.getCoverageData()
|
|
2540
3239
|
);
|
|
2541
|
-
const COVERAGE_REPORT_PATH = "coverage/lcov.info";
|
|
2542
3240
|
try {
|
|
2543
|
-
generateLcovReport(coverageData,
|
|
2544
|
-
console.log(
|
|
2545
|
-
`
|
|
2546
|
-
\u{1F4CA} Coverage report generated: ${COVERAGE_REPORT_PATH}`
|
|
2547
|
-
);
|
|
3241
|
+
generateLcovReport(coverageData, "coverage/lcov.info");
|
|
3242
|
+
console.log("\n\u{1F4CA} Coverage report generated: coverage/lcov.info");
|
|
2548
3243
|
} catch (error) {
|
|
2549
3244
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2550
3245
|
console.error(
|