Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] Expose collections from getters as XPath sequence attributes #4969

Merged
merged 29 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
53246d5
Support sequences in XPath Attributes
jsotuyod Apr 12, 2024
a01481a
Add test for collection attributes
jsotuyod Apr 19, 2024
f307e6e
Update changelog
jsotuyod Apr 19, 2024
8db0c80
Restrict exposed attributes based on element types
jsotuyod Apr 19, 2024
ce5e229
Produce deprecation warnings when atomize is used
jsotuyod Apr 19, 2024
8d51a2f
Just do it once per attribute
jsotuyod Apr 19, 2024
b74b6e5
Revert. Different rules on the same node report separately
jsotuyod Apr 19, 2024
0d6f196
Fix broken tests
jsotuyod Apr 19, 2024
3a4abd7
Schema awareness changes the produced queries
jsotuyod Apr 19, 2024
02e7a71
Update Apex tree dumps with the new attributes
jsotuyod Apr 19, 2024
d769336
Have the NodePrinter show collections as sequences
jsotuyod Apr 19, 2024
d9e146d
Update Java parser test to show modifiers properly
jsotuyod Apr 19, 2024
ca7e15c
update output to sequences
jsotuyod Apr 19, 2024
0d32ac8
Update more tree dump tests
jsotuyod Apr 19, 2024
62443a4
Update tree export test
jsotuyod Apr 19, 2024
1231a54
Remove unsused imports
jsotuyod Apr 19, 2024
346d7fd
Do not warn for List being deprecated by default
jsotuyod Apr 19, 2024
3650622
Remove unused imports
jsotuyod Apr 19, 2024
28c5cd7
Remove `@Names` attribute from ASTReferenceExpression
jsotuyod Apr 28, 2024
56f12d4
Update docs/pages/release_notes.md
jsotuyod Apr 28, 2024
11e6cc5
Update docs on writing xpath rules
jsotuyod Apr 28, 2024
124f908
Properly log when the impossible happens
jsotuyod Apr 28, 2024
fbb4648
Update pmd-core/src/main/java/net/sourceforge/pmd/lang/rule/xpath/int…
jsotuyod Apr 28, 2024
6660664
Merge branch 'master' into xpath-seq-attributes
jsotuyod Apr 28, 2024
458405e
Use spaces
jsotuyod Apr 28, 2024
4bb533b
Ignore type variables
jsotuyod Apr 28, 2024
895cfbf
Update docs/pages/release_notes.md
jsotuyod May 2, 2024
6fd230e
Update docs/pages/release_notes.md
jsotuyod May 2, 2024
722f25b
Merge branch 'master' into xpath-seq-attributes
jsotuyod May 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 12 additions & 14 deletions docs/pages/pmd/userdocs/extending/writing_xpath_rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,21 @@ To represent attributes, we must map Java values to [XPath Data Model (XDM)](htt
values. In the following table we refer to the type conversion function as `conv`, a function from Java types
to XDM types.

| Java type `T` | XSD type `conv(T)` |
|---------------|---------------------------------------|
| `int` | `xs:integer` |
| `long` | `xs:integer` |
| `double` | `xs:decimal` |
| `float` | `xs:decimal` |
| `boolean` | `xs:boolean` |
| `String` | `xs:string` |
| `Character` | `xs:string` |
| `Enum<E>` | `xs:string` (uses `Object::toString`) |
| `List<E>` | `conv(E)*` (a sequence type) |
| Java type `T` | XSD type `conv(T)` |
|-------------------|---------------------------------------|
| `int` | `xs:integer` |
| `long` | `xs:integer` |
| `double` | `xs:decimal` |
| `float` | `xs:decimal` |
| `boolean` | `xs:boolean` |
| `String` | `xs:string` |
| `Character` | `xs:string` |
| `Enum<E>` | `xs:string` (uses `Object::toString`) |
| `Collection<E>` | `conv(E)*` (a sequence type) |

The same `conv` function is used to translate rule property values to XDM values.

{% include warning.html content="Lists are only supported for rule properties, not attributes." %}


Additionaly, PMD's own `net.sourceforge.pmd.lang.document.Chars` is also translated to a `xs:string`


## Rule properties
Expand Down
12 changes: 11 additions & 1 deletion docs/pages/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,19 @@ This is a {{ site.pmd.release_type }} release.

### 🚀 New and noteworthy

### 🐛 Fixed Issues
#### Collections exposed as XPath attributes

Up to now, all AST node getters would be exposed to XPath, as long as the return type was a primitive (boxed or unboxed), String or Enum. That meant that collections, even of these basic types, were not exposed, so for instance accessing Apex's `ASTUserClass.getInterfaceNames()` to list the interfaces implemented by a class was impossible from XPath, and would require writing a Java rule to check it.
jsotuyod marked this conversation as resolved.
Show resolved Hide resolved

Since this release, PMD will also expose any getter returning a collection of any supported type as a sequence through an XPath attribute. They would require to use apropriate XQuery functions to manipulate the sequence. So for instance, to detect any given `ASTUserClass` in Apex that implements `Queueable`, it is now possible to write:

```xml
/UserClass[@InterfaceNames = 'Queueable']
```

### 🐛 Fixed Issues
* core
* [#4467](https://github.com/pmd/pmd/issues/4467): \[core] Expose collections from getters as XPath sequence attributes
* [#4978](https://github.com/pmd/pmd/issues/4978): \[core] Referenced Rulesets do not emit details on validation errors
* [#4983](https://github.com/pmd/pmd/pull/4983): \[cpd] Fix CPD crashes about unicode escapes
* java
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.util.List;
import java.util.stream.Collectors;

import net.sourceforge.pmd.lang.rule.xpath.NoAttribute;

import com.google.summit.ast.Identifier;

public final class ASTReferenceExpression extends AbstractApexNode.Many<Identifier> {
Expand Down Expand Up @@ -38,6 +40,7 @@ public String getImage() {
return "";
}

@NoAttribute
public List<String> getNames() {
return nodes.stream().map(Identifier::getString).collect(Collectors.toList());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
+- ApexFile[@DefiningType = "InnerClassLocations", @RealLoc = true]
+- UserClass[@DefiningType = "InnerClassLocations", @Image = "InnerClassLocations", @RealLoc = true, @SimpleName = "InnerClassLocations", @SuperClassName = ""]
+- UserClass[@DefiningType = "InnerClassLocations", @Image = "InnerClassLocations", @InterfaceNames = (), @RealLoc = true, @SimpleName = "InnerClassLocations", @SuperClassName = ""]
+- ModifierNode[@Abstract = false, @DefiningType = "InnerClassLocations", @DeprecatedTestMethod = false, @Final = false, @Global = false, @InheritedSharing = false, @Modifiers = 1, @Override = false, @Private = false, @Protected = false, @Public = true, @RealLoc = true, @Static = false, @Test = false, @TestOrTestSetup = false, @Transient = false, @Virtual = false, @WebService = false, @WithSharing = false, @WithoutSharing = false]
+- UserClass[@DefiningType = "InnerClassLocations.bar1", @Image = "bar1", @RealLoc = true, @SimpleName = "bar1", @SuperClassName = ""]
+- UserClass[@DefiningType = "InnerClassLocations.bar1", @Image = "bar1", @InterfaceNames = (), @RealLoc = true, @SimpleName = "bar1", @SuperClassName = ""]
| +- ModifierNode[@Abstract = false, @DefiningType = "InnerClassLocations.bar1", @DeprecatedTestMethod = false, @Final = false, @Global = false, @InheritedSharing = false, @Modifiers = 1, @Override = false, @Private = false, @Protected = false, @Public = true, @RealLoc = true, @Static = false, @Test = false, @TestOrTestSetup = false, @Transient = false, @Virtual = false, @WebService = false, @WithSharing = false, @WithoutSharing = false]
| +- Method[@Arity = 0, @CanonicalName = "m", @Constructor = false, @DefiningType = "InnerClassLocations.bar1", @Image = "m", @RealLoc = true, @ReturnType = "void", @StaticInitializer = false]
| +- ModifierNode[@Abstract = false, @DefiningType = "InnerClassLocations.bar1", @DeprecatedTestMethod = false, @Final = false, @Global = false, @InheritedSharing = false, @Modifiers = 1, @Override = false, @Private = false, @Protected = false, @Public = true, @RealLoc = true, @Static = false, @Test = false, @TestOrTestSetup = false, @Transient = false, @Virtual = false, @WebService = false, @WithSharing = false, @WithoutSharing = false]
Expand All @@ -14,7 +14,7 @@
| +- MethodCallExpression[@DefiningType = "InnerClassLocations.bar1", @FullMethodName = "System.out.println", @InputParametersSize = 1, @MethodName = "println", @RealLoc = true]
| +- ReferenceExpression[@DefiningType = "InnerClassLocations.bar1", @Image = "System", @RealLoc = true, @ReferenceType = ReferenceType.METHOD, @SObjectType = false, @SafeNav = false]
| +- LiteralExpression[@Boolean = false, @Decimal = false, @DefiningType = "InnerClassLocations.bar1", @Double = false, @Image = "foo", @Integer = false, @LiteralType = LiteralType.STRING, @Long = false, @Name = null, @Null = false, @RealLoc = true, @String = true]
+- UserClass[@DefiningType = "InnerClassLocations.bar2", @Image = "bar2", @RealLoc = true, @SimpleName = "bar2", @SuperClassName = ""]
+- UserClass[@DefiningType = "InnerClassLocations.bar2", @Image = "bar2", @InterfaceNames = (), @RealLoc = true, @SimpleName = "bar2", @SuperClassName = ""]
+- ModifierNode[@Abstract = false, @DefiningType = "InnerClassLocations.bar2", @DeprecatedTestMethod = false, @Final = false, @Global = false, @InheritedSharing = false, @Modifiers = 1, @Override = false, @Private = false, @Protected = false, @Public = true, @RealLoc = true, @Static = false, @Test = false, @TestOrTestSetup = false, @Transient = false, @Virtual = false, @WebService = false, @WithSharing = false, @WithoutSharing = false]
+- Method[@Arity = 0, @CanonicalName = "m", @Constructor = false, @DefiningType = "InnerClassLocations.bar2", @Image = "m", @RealLoc = true, @ReturnType = "void", @StaticInitializer = false]
+- ModifierNode[@Abstract = false, @DefiningType = "InnerClassLocations.bar2", @DeprecatedTestMethod = false, @Final = false, @Global = false, @InheritedSharing = false, @Modifiers = 1, @Override = false, @Private = false, @Protected = false, @Public = true, @RealLoc = true, @Static = false, @Test = false, @TestOrTestSetup = false, @Transient = false, @Virtual = false, @WebService = false, @WithSharing = false, @WithoutSharing = false]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
+- ApexFile[@DefiningType = "NullCoalescingOperator", @RealLoc = true]
+- UserClass[@DefiningType = "NullCoalescingOperator", @Image = "NullCoalescingOperator", @RealLoc = true, @SimpleName = "NullCoalescingOperator", @SuperClassName = ""]
+- UserClass[@DefiningType = "NullCoalescingOperator", @Image = "NullCoalescingOperator", @InterfaceNames = (), @RealLoc = true, @SimpleName = "NullCoalescingOperator", @SuperClassName = ""]
+- ModifierNode[@Abstract = false, @DefiningType = "NullCoalescingOperator", @DeprecatedTestMethod = false, @Final = false, @Global = false, @InheritedSharing = false, @Modifiers = 1, @Override = false, @Private = false, @Protected = false, @Public = true, @RealLoc = true, @Static = false, @Test = false, @TestOrTestSetup = false, @Transient = false, @Virtual = false, @WebService = false, @WithSharing = false, @WithoutSharing = false]
+- Method[@Arity = 2, @CanonicalName = "leftOrRight", @Constructor = false, @DefiningType = "NullCoalescingOperator", @Image = "leftOrRight", @RealLoc = true, @ReturnType = "String", @StaticInitializer = false]
+- ModifierNode[@Abstract = false, @DefiningType = "NullCoalescingOperator", @DeprecatedTestMethod = false, @Final = false, @Global = false, @InheritedSharing = false, @Modifiers = 1, @Override = false, @Private = false, @Protected = false, @Public = true, @RealLoc = true, @Static = false, @Test = false, @TestOrTestSetup = false, @Transient = false, @Virtual = false, @WebService = false, @WithSharing = false, @WithoutSharing = false]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
+- ApexFile[@DefiningType = "Foo", @RealLoc = true]
+- UserClass[@DefiningType = "Foo", @Image = "Foo", @RealLoc = true, @SimpleName = "Foo", @SuperClassName = ""]
+- UserClass[@DefiningType = "Foo", @Image = "Foo", @InterfaceNames = (), @RealLoc = true, @SimpleName = "Foo", @SuperClassName = ""]
+- ModifierNode[@Abstract = false, @DefiningType = "Foo", @DeprecatedTestMethod = false, @Final = false, @Global = false, @InheritedSharing = false, @Modifiers = 1, @Override = false, @Private = false, @Protected = false, @Public = true, @RealLoc = true, @Static = false, @Test = false, @TestOrTestSetup = false, @Transient = false, @Virtual = false, @WebService = false, @WithSharing = false, @WithoutSharing = false]
+- Field[@DefiningType = "Foo", @Image = "x", @Name = "x", @RealLoc = true, @Type = "Integer", @Value = null]
| +- ModifierNode[@Abstract = false, @DefiningType = "Foo", @DeprecatedTestMethod = false, @Final = false, @Global = false, @InheritedSharing = false, @Modifiers = 0, @Override = false, @Private = false, @Protected = false, @Public = false, @RealLoc = false, @Static = false, @Test = false, @TestOrTestSetup = false, @Transient = false, @Virtual = false, @WebService = false, @WithSharing = false, @WithoutSharing = false]
+- Field[@DefiningType = "Foo", @Image = "profileUrl", @Name = "profileUrl", @RealLoc = true, @Type = "String", @Value = null]
| +- ModifierNode[@Abstract = false, @DefiningType = "Foo", @DeprecatedTestMethod = false, @Final = false, @Global = false, @InheritedSharing = false, @Modifiers = 0, @Override = false, @Private = false, @Protected = false, @Public = false, @RealLoc = false, @Static = false, @Test = false, @TestOrTestSetup = false, @Transient = false, @Virtual = false, @WebService = false, @WithSharing = false, @WithoutSharing = false]
+- FieldDeclarationStatements[@DefiningType = "Foo", @RealLoc = true, @TypeName = "Integer"]
+- FieldDeclarationStatements[@DefiningType = "Foo", @RealLoc = true, @TypeArguments = (), @TypeName = "Integer"]
| +- ModifierNode[@Abstract = false, @DefiningType = "Foo", @DeprecatedTestMethod = false, @Final = false, @Global = false, @InheritedSharing = false, @Modifiers = 0, @Override = false, @Private = false, @Protected = false, @Public = false, @RealLoc = false, @Static = false, @Test = false, @TestOrTestSetup = false, @Transient = false, @Virtual = false, @WebService = false, @WithSharing = false, @WithoutSharing = false]
| +- FieldDeclaration[@DefiningType = "Foo", @Image = "x", @Name = "x", @RealLoc = true]
| +- VariableExpression[@DefiningType = "Foo", @Image = "anIntegerField", @RealLoc = true]
Expand All @@ -14,7 +14,7 @@
| | +- EmptyReferenceExpression[@DefiningType = null, @RealLoc = false]
| +- VariableExpression[@DefiningType = "Foo", @Image = "x", @RealLoc = true]
| +- EmptyReferenceExpression[@DefiningType = null, @RealLoc = false]
+- FieldDeclarationStatements[@DefiningType = "Foo", @RealLoc = true, @TypeName = "String"]
+- FieldDeclarationStatements[@DefiningType = "Foo", @RealLoc = true, @TypeArguments = (), @TypeName = "String"]
| +- ModifierNode[@Abstract = false, @DefiningType = "Foo", @DeprecatedTestMethod = false, @Final = false, @Global = false, @InheritedSharing = false, @Modifiers = 0, @Override = false, @Private = false, @Protected = false, @Public = false, @RealLoc = false, @Static = false, @Test = false, @TestOrTestSetup = false, @Transient = false, @Virtual = false, @WebService = false, @WithSharing = false, @WithoutSharing = false]
| +- FieldDeclaration[@DefiningType = "Foo", @Image = "profileUrl", @Name = "profileUrl", @RealLoc = true]
| +- MethodCallExpression[@DefiningType = "Foo", @FullMethodName = "toExternalForm", @InputParametersSize = 0, @MethodName = "toExternalForm", @RealLoc = true]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ void testReadStandardInput() throws Exception {
final CliExecutionResult output = runCliSuccessfully("-i", "-f", "xml", "-PlineSeparator=LF");

output.checkStdOut(equalTo("<?xml version='1.0' encoding='UTF-8' ?>\n"
+ "<dummyRootNode Image=''>\n"
+ " <dummyNode Image='a'>\n"
+ " <dummyNode Image='b' />\n"
+ "<dummyRootNode Image='' Lines='[, , ]'>\n"
+ " <dummyNode Image='a' Lines='[a, a, a]'>\n"
+ " <dummyNode Image='b' Lines='[b, b, b]' />\n"
+ " </dummyNode>\n"
+ "</dummyRootNode>\n"));
});
Expand All @@ -44,9 +44,9 @@ void testReadFile() throws Exception {
File file = newFileWithContents("(a(b))");
final CliExecutionResult result = runCliSuccessfully("--file", file.getAbsolutePath(), "-f", "xml", "-PlineSeparator=LF");
result.checkStdOut(equalTo("<?xml version='1.0' encoding='UTF-8' ?>\n"
+ "<dummyRootNode Image=''>\n"
+ " <dummyNode Image='a'>\n"
+ " <dummyNode Image='b' />\n"
+ "<dummyRootNode Image='' Lines='[, , ]'>\n"
oowekyala marked this conversation as resolved.
Show resolved Hide resolved
+ " <dummyNode Image='a' Lines='[a, a, a]'>\n"
+ " <dummyNode Image='b' Lines='[b, b, b]' />\n"
+ " </dummyNode>\n"
+ "</dummyRootNode>\n"));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Objects;

import org.checkerframework.checker.nullness.qual.NonNull;
Expand Down Expand Up @@ -106,16 +105,11 @@ String replacementIfDeprecated() {
return null;
} else {
DeprecatedAttribute annot = method.getAnnotation(DeprecatedAttribute.class);
String result = annot != null
return annot != null
? annot.replaceWith()
: method.isAnnotationPresent(Deprecated.class)
? DeprecatedAttribute.NO_REPLACEMENT
: null;
if (result == null && List.class.isAssignableFrom(method.getReturnType())) {
// Lists are generally deprecated, see #2451
result = DeprecatedAttribute.NO_REPLACEMENT;
}
return result;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
Expand Down Expand Up @@ -119,7 +123,27 @@ && isConsideredReturnType(method)

private boolean isConsideredReturnType(Method method) {
Class<?> klass = method.getReturnType();
return CONSIDERED_RETURN_TYPES.contains(klass) || klass.isEnum();
if (CONSIDERED_RETURN_TYPES.contains(klass) || klass.isEnum()) {
return true;
}

if (Collection.class.isAssignableFrom(klass)) {
Type t = method.getGenericReturnType();
if (t instanceof ParameterizedType) {
try {
// ignore type variables, such as List<N>… we could check all bounds, but probably it's overkill
Type actualTypeArgument = ((ParameterizedType) t).getActualTypeArguments()[0];
if (!TypeVariable.class.isAssignableFrom(actualTypeArgument.getClass())) {
Class<?> elementKlass = Class.forName(actualTypeArgument.getTypeName());
return CONSIDERED_RETURN_TYPES.contains(elementKlass) || elementKlass.isEnum();
}
} catch (ClassNotFoundException e) {
throw AssertionUtil.shouldNotReachHere("Method '" + method + "' should return a known type, but: " + e, e);
}
}
}

return false;
}

private boolean isIgnored(Class<?> nodeClass, Method method) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ protected AxisIterator iterateSiblings(NodeTest nodeTest, boolean forwards) {

@Override
public AtomicSequence atomize() {
getTreeInfo().getLogger().recordUsageOf(attribute);
if (value == null) {
value = DomainConversion.convert(attribute.getValue());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,14 @@ public static NamePool getNamePool() {
return NAME_POOL;
}


final class StaticContextWithProperties extends IndependentContext {

private final Map<StructuredQName, PropertyDescriptor<?>> propertiesByName = new HashMap<>();

StaticContextWithProperties(Configuration config) {
super(config);
// This statement is necessary for Saxon to support sequence-valued attributes
getPackageData().setSchemaAware(true);
jsotuyod marked this conversation as resolved.
Show resolved Hide resolved
}

public void declareProperty(PropertyDescriptor<?> prop) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package net.sourceforge.pmd.lang.ast;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -125,6 +126,11 @@ public Iterator<Attribute> getXPathAttributesIterator() {
return attributes.iterator();
}

// phony attribute that repeats the image 3 times
public List<String> getLines() {
return Arrays.asList(getImage(), getImage(), getImage());
}

public static class DummyRootNode extends DummyNode implements RootNode, GenericNode<DummyNode> {

// FIXME remove this
Expand Down