test-pmd-rule 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -162,6 +162,10 @@ public class TestClass {
162
162
  1. **Extraction**: The tool reads all `<example>` tags from your rule XML file
163
163
  2. **Parsing**: Each example is parsed to identify violation and valid code sections
164
164
  3. **Test File Creation**: Temporary Apex test files are generated with the example code
165
+ - If an example contains a single top-level class, it's renamed to a test class name
166
+ - If an example contains multiple top-level classes, they're wrapped as inner classes within a test class
167
+ - If an example contains only methods/fields without a class, they're wrapped in a test class
168
+ - If an example contains only standalone code, it's wrapped in a test method within a test class
165
169
  4. **PMD Execution**: PMD is run against the test files to check if violations occur as expected
166
170
  5. **Validation**: The tool verifies that:
167
171
  - Violation examples actually trigger the rule
@@ -213,6 +217,7 @@ The AST nodes are color-coded to indicate which parts of your example code are b
213
217
  - **Dark Red (dim)**: Node is tested by **violation** examples but was already covered in previous examples
214
218
  - **Green (bright)**: Node is tested by **valid** examples in the current example
215
219
  - **Dark Green (dim)**: Node is tested by **valid** examples but was already covered in previous examples
220
+ - **Orange**: Node matches both **violation** and **valid** sections (indicates ambiguity - the same code appears in both sections)
216
221
  - **No color**: Node is not tested or couldn't be matched to example code
217
222
 
218
223
  This color coding helps you understand:
@@ -220,6 +225,7 @@ This color coding helps you understand:
220
225
  - Which AST nodes correspond to your violation/valid markers
221
226
  - Whether nodes are being tested by the current example or were already covered
222
227
  - Which parts of the AST structure your XPath expression should target
228
+ - When the same code appears in both violation and valid sections (orange indicates this ambiguity)
223
229
 
224
230
  **Notes:**
225
231
 
@@ -228,6 +234,7 @@ This color coding helps you understand:
228
234
  - If the generated test file has syntax errors, the tool will display the generated file content to help debug issues
229
235
  - The AST output includes all node attributes and can be quite verbose - use it when you need detailed insight into PMD's parsing
230
236
  - Colors use ANSI escape codes and will automatically be disabled if your terminal doesn't support them
237
+ - **Wrapper class cleanup**: The tool automatically strips wrapper class prefixes from `DefiningType` attributes (e.g., `DefiningType='TestClass1.MyClassViolation'` becomes `DefiningType='MyClassViolation'`) to make the output cleaner and more readable
231
238
 
232
239
  ## Requirements
233
240
 
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/main.ts
4
- import { existsSync as existsSync2, readdirSync, statSync } from "fs";
4
+ import { existsSync as existsSync2, readdirSync, realpathSync as realpathSync2, statSync } from "fs";
5
5
  import { extname, resolve as resolve3 } from "path";
6
6
  import { argv } from "process";
7
7
  import { cpus } from "os";
8
- import { pathToFileURL } from "url";
8
+ import { fileURLToPath } from "url";
9
9
  import { DOMParser as DOMParser4 } from "@xmldom/xmldom";
10
10
  import { stringifyTree } from "stringify-tree";
11
11
 
@@ -1392,6 +1392,7 @@ var SPACE_CHAR = " ";
1392
1392
  var DEFAULT_INDENT = " ";
1393
1393
  var FIRST_ELEMENT_INDEX2 = 0;
1394
1394
  var LAST_ELEMENT_OFFSET = 1;
1395
+ var SINGLE_COUNT = 1;
1395
1396
  function removeInlineMarkers(line) {
1396
1397
  if (line.includes("// \u274C")) {
1397
1398
  const splitResult = line.split("// \u274C");
@@ -1611,16 +1612,15 @@ function createTestFile({
1611
1612
  const TEST_CLASS_NAME = `TestClass${String(exampleIndex)}`;
1612
1613
  const fullExampleContent = parsed.content;
1613
1614
  const ZERO_BRACE_DEPTH = 0;
1614
- const hasTopLevelClass = (() => {
1615
- let foundTopLevelClass = false;
1615
+ const topLevelClassCount = (() => {
1616
+ let count = 0;
1616
1617
  let braceDepth = ZERO_BRACE_DEPTH;
1617
1618
  const exampleLines = fullExampleContent.split("\n");
1618
1619
  for (const line of exampleLines) {
1619
1620
  const trimmed = line.trim();
1620
1621
  const isClassDef = trimmed.startsWith("public class ") || trimmed.startsWith("class ") || trimmed.startsWith("private class ");
1621
1622
  if (isClassDef && braceDepth === ZERO_BRACE_DEPTH) {
1622
- foundTopLevelClass = true;
1623
- break;
1623
+ count++;
1624
1624
  }
1625
1625
  const openBracesMatch = line.match(/{/g);
1626
1626
  const closeBracesMatch = line.match(/}/g);
@@ -1628,12 +1628,20 @@ function createTestFile({
1628
1628
  const closeBraces = closeBracesMatch ? closeBracesMatch.length : ZERO_BRACE_DEPTH;
1629
1629
  braceDepth += openBraces - closeBraces;
1630
1630
  }
1631
- return foundTopLevelClass;
1631
+ return count;
1632
1632
  })();
1633
+ const classDefCountInIncluded = codeToInclude.filter((line) => {
1634
+ const trimmed = line.trim();
1635
+ return trimmed.startsWith("public class ") || trimmed.startsWith("class ") || trimmed.startsWith("private class ");
1636
+ }).length;
1637
+ const hasTopLevelClass = topLevelClassCount === SINGLE_COUNT || topLevelClassCount > SINGLE_COUNT && classDefCountInIncluded <= SINGLE_COUNT;
1633
1638
  const hasClassLikeStructures = (() => {
1634
1639
  if (hasTopLevelClass) {
1635
1640
  return false;
1636
1641
  }
1642
+ if (topLevelClassCount > SINGLE_COUNT) {
1643
+ return classDefCountInIncluded > SINGLE_COUNT;
1644
+ }
1637
1645
  const fieldDeclarationRegex = /^\s*(public|private|protected)\s+\w+\s+\w+\s*[;=]/;
1638
1646
  return codeToInclude.some((line) => {
1639
1647
  const trimmed = line.trim();
@@ -3619,12 +3627,20 @@ var NODE_TYPE_ELEMENT = 1;
3619
3627
  var EMPTY_ARRAY_LENGTH = 0;
3620
3628
  var SINGLE_ELEMENT_INDEX = 0;
3621
3629
  var MIN_ARRAY_INDEX = 0;
3630
+ var DOT_SEPARATOR_LENGTH = 1;
3622
3631
  function isCliInvocation() {
3623
3632
  const [, entryPath] = argv;
3624
3633
  if (entryPath === void 0) {
3625
3634
  return false;
3626
3635
  }
3627
- return import.meta.url === pathToFileURL(entryPath).href;
3636
+ try {
3637
+ const currentModulePath = fileURLToPath(import.meta.url);
3638
+ const resolvedEntryPath = resolve3(process.cwd(), entryPath);
3639
+ const absoluteEntryPath = realpathSync2(resolvedEntryPath);
3640
+ return currentModulePath === absoluteEntryPath;
3641
+ } catch {
3642
+ return false;
3643
+ }
3628
3644
  }
3629
3645
  function findXmlFiles(directory) {
3630
3646
  const xmlFiles = [];
@@ -3760,8 +3776,16 @@ function removeWrappersFromXmlDom(doc, exampleIndex, wrapperInfo) {
3760
3776
  const allNodes2 = doc.getElementsByTagName("*");
3761
3777
  for (const node of Array.from(allNodes2)) {
3762
3778
  const definingType = node.getAttribute("DefiningType");
3779
+ if (definingType === null) {
3780
+ continue;
3781
+ }
3763
3782
  if (definingType === wrapperClassName2) {
3764
3783
  node.removeAttribute("DefiningType");
3784
+ } else if (definingType.startsWith(`${wrapperClassName2}.`)) {
3785
+ const classNameWithoutPrefix = definingType.slice(
3786
+ wrapperClassName2.length + DOT_SEPARATOR_LENGTH
3787
+ );
3788
+ node.setAttribute("DefiningType", classNameWithoutPrefix);
3765
3789
  }
3766
3790
  }
3767
3791
  return;
@@ -3921,8 +3945,16 @@ function removeWrappersFromXmlDom(doc, exampleIndex, wrapperInfo) {
3921
3945
  const allNodes = doc.getElementsByTagName("*");
3922
3946
  for (const node of Array.from(allNodes)) {
3923
3947
  const definingType = node.getAttribute("DefiningType");
3948
+ if (definingType === null) {
3949
+ continue;
3950
+ }
3924
3951
  if (definingType === wrapperClassName) {
3925
3952
  node.removeAttribute("DefiningType");
3953
+ } else if (definingType.startsWith(`${wrapperClassName}.`)) {
3954
+ const classNameWithoutPrefix = definingType.slice(
3955
+ wrapperClassName.length + DOT_SEPARATOR_LENGTH
3956
+ );
3957
+ node.setAttribute("DefiningType", classNameWithoutPrefix);
3926
3958
  }
3927
3959
  }
3928
3960
  }
@@ -4052,16 +4084,18 @@ function determineNodeColor(options) {
4052
4084
  return void 0;
4053
4085
  }
4054
4086
  }
4055
- let hasViolationSection = false;
4056
- let hasValidSection = false;
4087
+ let violationMatchCount = 0;
4088
+ let validMatchCount = 0;
4057
4089
  for (const lineNum of matchingLines) {
4058
4090
  const section = lineSectionMap.get(lineNum);
4059
4091
  if (section === "violation") {
4060
- hasViolationSection = true;
4092
+ violationMatchCount++;
4061
4093
  } else if (section === "valid") {
4062
- hasValidSection = true;
4094
+ validMatchCount++;
4063
4095
  }
4064
4096
  }
4097
+ const hasViolationSection = violationMatchCount > EMPTY_MARKERS_LENGTH;
4098
+ const hasValidSection = validMatchCount > EMPTY_MARKERS_LENGTH;
4065
4099
  let alreadyCovered = false;
4066
4100
  if (xpath !== null && previousExamplesCoverage !== void 0 && previousExamplesCoverage.length > EMPTY_COVERAGE_LENGTH) {
4067
4101
  const nodeTypeInXPath = xpath.includes(nodeType);
@@ -4140,10 +4174,19 @@ function determineNodeColor(options) {
4140
4174
  }
4141
4175
  }
4142
4176
  }
4143
- if (hasViolationSection && violationMarkers.length > EMPTY_MARKERS_LENGTH) {
4177
+ if (hasViolationSection && hasValidSection) {
4178
+ if (violationMarkers.length > EMPTY_MARKERS_LENGTH && validMarkers.length > EMPTY_MARKERS_LENGTH) {
4179
+ return "orange";
4180
+ }
4181
+ if (violationMarkers.length > EMPTY_MARKERS_LENGTH) {
4182
+ return alreadyCovered ? "dark-red" : "red";
4183
+ }
4184
+ if (validMarkers.length > EMPTY_MARKERS_LENGTH) {
4185
+ return alreadyCovered ? "dark-green" : "green";
4186
+ }
4187
+ } else if (hasViolationSection && violationMarkers.length > EMPTY_MARKERS_LENGTH) {
4144
4188
  return alreadyCovered ? "dark-red" : "red";
4145
- }
4146
- if (hasValidSection && validMarkers.length > EMPTY_MARKERS_LENGTH) {
4189
+ } else if (hasValidSection && validMarkers.length > EMPTY_MARKERS_LENGTH) {
4147
4190
  return alreadyCovered ? "dark-green" : "green";
4148
4191
  }
4149
4192
  return void 0;
@@ -4243,6 +4286,7 @@ function applyColorToNodeName(nodeName, color) {
4243
4286
  const ANSI_DARK_GREEN = "\x1B[32;2m";
4244
4287
  const ANSI_RED = "\x1B[31m";
4245
4288
  const ANSI_DARK_RED = "\x1B[31;2m";
4289
+ const ANSI_ORANGE = "\x1B[33m";
4246
4290
  if (color === "green") {
4247
4291
  return `${ANSI_GREEN}${nodeName}${ANSI_RESET}`;
4248
4292
  }
@@ -4252,7 +4296,10 @@ function applyColorToNodeName(nodeName, color) {
4252
4296
  if (color === "red") {
4253
4297
  return `${ANSI_RED}${nodeName}${ANSI_RESET}`;
4254
4298
  }
4255
- return `${ANSI_DARK_RED}${nodeName}${ANSI_RESET}`;
4299
+ if (color === "dark-red") {
4300
+ return `${ANSI_DARK_RED}${nodeName}${ANSI_RESET}`;
4301
+ }
4302
+ return `${ANSI_ORANGE}${nodeName}${ANSI_RESET}`;
4256
4303
  }
4257
4304
  function parseXmlAstAndStripWrappers(options) {
4258
4305
  const {