# XPath Patterns for Apex Custom Rules Pre-validated PMD XPath patterns for Salesforce Apex code. Every pattern below has been verified against actual `sf code-analyzer ast-dump` output. Use these directly — but still run `ast-dump` on YOUR code to confirm node names haven't changed in newer PMD versions. ## XPath Syntax Quick Reference | Syntax | Meaning | |--------|---------| | `//Node` | Find Node anywhere in tree | | `//Node[@attr='value']` | Node with specific string attribute value | | `//Node[@attr = true()]` | Node with boolean attribute = true (**PMD 7 requirement**) | | `//Parent//Child` | Child anywhere inside Parent (any descendant) | | `//Parent/Child` | **Direct child only** — use this to avoid false positives | | `//Node[not(...)]` | Node where condition is NOT true | | `//Node[ancestor::Other]` | Node that has Other as an ancestor | | `//Node[.//Other]` | Node that contains Other somewhere inside | | `@Image` | The name/text of the node | | `@FullMethodName` | Full qualified method name (e.g., 'System.debug') | | `@LiteralType` | Type of literal: STRING, INTEGER, BOOLEAN, TRUE, FALSE, NULL | ### PMD 7 Boolean Attributes — MUST use `true()` / `false()` In PMD 7, many node attributes are **boolean typed**, not strings — even though `ast-dump` renders them as `Nested='false'` or `Test='true'` in the XML output. String attributes (`@FullMethodName`, `@Name`, `@Image`, `@LiteralType`, etc.) are safe to copy from the AST dump as-is. But **attributes whose values are `true` or `false` in the dump are boolean-typed** — comparing them with string literals (`@Nested='false'`) causes the error: "Cannot compare xs:boolean to xs:string". Known boolean attributes by node: | Node | Boolean Attributes | |------|--------------------| | `ModifierNode` | `@Test`, `@Public`, `@Private`, `@Protected`, `@Static`, `@Abstract`, `@Final`, `@Global`, `@WithSharing`, `@WithoutSharing`, `@InheritedSharing` | | `UserClass` | `@Nested` | | `Method` | `@Constructor` | | WRONG | CORRECT | |-------|---------| | `UserClass[@Nested='false']` | `UserClass[@Nested = false()]` | | `UserClass[@Nested='true']` | `UserClass[@Nested = true()]` | | `ModifierNode[@Test='true']` | `ModifierNode[@Test = true()]` | | `ModifierNode[@Static='false']` | `ModifierNode[@Static = false()]` | | `Method[@Constructor = false()]` | `Method[@Constructor = false()]` | `true()` and `false()` are XPath boolean functions. If you see an attribute that looks boolean in the AST dump, assume it is typed as boolean and use `true()`/`false()`. --- ### `/` vs `//` — The Most Common Source of False Positives | XPath | Meaning | Risk | |-------|---------|------| | `//ForEachStatement//SoqlExpression` | SOQL anywhere inside ForEachStatement | ❌ Matches iterable position too | | `//ForEachStatement/BlockStatement//SoqlExpression` | SOQL inside loop **body** only | ✅ Correct | **Rule of thumb:** When matching inside a structural node (loop, if, try), always scope to `/BlockStatement//` to target the body, not sibling children like iterables or conditions. --- ## Pattern Categories (By Topic) For detailed patterns in each category, see the dedicated files below: | Category | File | Contents | |----------|------|----------| | Governor Limits | [xpath-patterns-governor-limits.md](xpath-patterns-governor-limits.md) | SOQL/DML in loops, Database methods in loops | | Method Calls & Annotations | [xpath-patterns-method-calls.md](xpath-patterns-method-calls.md) | Ban specific methods, @AuraEnabled, @future, @IsTest, @SuppressWarnings patterns | | Security | [xpath-patterns-security.md](xpath-patterns-security.md) | Sharing declarations, SOQL security modes, hardcoded IDs | | Code Structure, Tests & Naming | [xpath-patterns-structure.md](xpath-patterns-structure.md) | DML error handling, empty catches, test assertions, naming conventions | --- ## Key Apex AST Node Names | Apex Construct | AST Node | Key Attributes | |---|---|---| | Class | `UserClass` | `Image`, `SuperClassName`, `InterfaceNames`, `Nested` | | Interface | `UserInterface` | `Image`, `SuperInterfaceName` | | Method | `Method` | `Image`, `Arity`, `ReturnType`, `Constructor` | | Trigger | `UserTrigger` | `Image`, `TargetName` | | SOQL query | `SoqlExpression` | `Query`, `CanonicalQuery` | | DML insert | `DmlInsertStatement` | | | DML update | `DmlUpdateStatement` | | | DML delete | `DmlDeleteStatement` | | | DML upsert | `DmlUpsertStatement` | | | Method call | `MethodCallExpression` | `FullMethodName`, `MethodName`, `InputParametersSize` | | For-each loop | `ForEachStatement` | children: VariableDeclarationStatements, VariableExpression, **BlockStatement** (body), iterable | | For loop | `ForLoopStatement` | children: init, condition, update, **BlockStatement** (body) | | While loop | `WhileLoopStatement` | children: condition, **BlockStatement** (body) | | If/else | `IfElseBlockStatement` > `IfBlockStatement` | `ElseStatement` | | Try-catch | `TryCatchFinallyBlockStatement` | | | Catch block | `CatchBlockStatement` | `ExceptionType`, `VariableName` | | RunAs | `RunAsBlockStatement` | | | String literal | `LiteralExpression` | `@LiteralType='STRING'`, `@Image` (value without quotes) | | Integer literal | `LiteralExpression` | `@LiteralType='INTEGER'`, `@Image` | | Boolean true | `LiteralExpression` | `@LiteralType='TRUE'` | | Boolean false | `LiteralExpression` | `@LiteralType='FALSE'` | | Null | `LiteralExpression` | `@LiteralType='NULL'` | | Variable | `VariableExpression` | `@Image` (name) | | Assignment | `AssignmentExpression` | `@Op` (=, +=, etc.) | | Binary expression | `BinaryExpression` | `@Op` (+, -, *, /) | | Boolean expression | `BooleanExpression` | `@Op` (>, <, ==, !=, >=, <=) | | New object | `NewKeyValueObjectExpression` | `@Type` | | New object (no-arg) | `NewObjectExpression` | `@Type` | | Return | `ReturnStatement` | | | Annotation | `Annotation` | `@Name` (IsTest, AuraEnabled, Future, SuppressWarnings) | | Annotation param | `AnnotationParameter` | `@Name`, `@Value` | | Modifier | `ModifierNode` | `Public`, `Private`, `Static`, `Test`, `WithSharing`, `Global`, etc. | | Parameter | `Parameter` | `@Image` (name), `@Type` | | New list | `NewListInitExpression` | | | New map | `NewMapInitExpression` | | --- ## XPath Best Practices 1. **ALWAYS run `ast-dump` first** — never guess node names, even for patterns listed here 2. **Use `/BlockStatement//` for loop/if body** — avoids matching iterables, conditions, etc. 3. **Use `@FullMethodName`** for method calls — not `@Image` or `@MethodName` alone 4. **Exclude test classes** with `[not(ancestor::UserClass[ModifierNode[@Test = true()]])]` 5. **Test with BOTH positive AND negative cases** — ensure no false positives 6. **Prefer `//Node` over absolute paths** — code structure varies 7. **Use `ancestor::` / `not(ancestor::)`** for structural exclusions (try-catch, test class) 8. **Keep XPath simple** — complex expressions are fragile and hard to maintain ## Common False Positive Traps | Pattern | Trap | Fix | |---------|------|-----| | SOQL in loop | `//ForEachStatement//SoqlExpression` matches iterable | Use `/BlockStatement//` | | DML in loop | Same as above | Use `/BlockStatement//` | | Ban method in all code | Flags test code too | Add `[not(ancestor::UserClass[ModifierNode[@Test = true()]])]` | | Empty block detection | Matches intentional empty constructors | Add `[@Constructor = false()]` or exclude specific patterns | | No sharing declaration | Flags inner classes (which inherit) | Add `[@Nested = false()]` | | String literal length check | Matches test data strings | Exclude test classes |