Skip to content

Commit 97d141d

Browse files
adangeloowekyala
andcommitted
[core][java] Expose Chars attributes in XPath
- taken from #4352 (avoid getImage()) - Exposes @LiteralText for ASTLiteral Co-authored-by: Clément Fournier <[email protected]>
1 parent 73fcf6e commit 97d141d

File tree

55 files changed

+900
-840
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+900
-840
lines changed

pmd-core/src/main/java/net/sourceforge/pmd/lang/rule/xpath/Attribute.java

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import java.util.List;
1111
import java.util.Objects;
1212

13+
import org.checkerframework.checker.nullness.qual.NonNull;
14+
import org.checkerframework.checker.nullness.qual.Nullable;
1315
import org.slf4j.Logger;
1416
import org.slf4j.LoggerFactory;
1517

@@ -24,55 +26,73 @@
2426
* <p>Two attributes are equal if they have the same name
2527
* and their parent nodes are equal.
2628
*
27-
* @author daniels
29+
* <p>Note that attributes do not support just any type, but
30+
* a restricted set of value types that can be mapped to XPath types.
31+
* The exact supported types are not specified, but include at
32+
* least Java primitives and String.
33+
*
34+
* @see Node#getXPathAttributesIterator()
2835
*/
29-
public class Attribute {
36+
public final class Attribute {
3037
private static final Logger LOG = LoggerFactory.getLogger(Attribute.class);
3138

32-
private final Node parent;
33-
private final String name;
39+
private final @NonNull Node parent;
40+
private final @NonNull String name;
3441

35-
private final MethodHandle handle;
36-
private final Method method;
42+
private final @Nullable MethodHandle handle;
43+
private final @Nullable Method method;
44+
/** If true, we won't invoke the method handle again. */
3745
private boolean invoked;
3846

39-
private Object value;
47+
/** May be null after invocation too. */
48+
private @Nullable Object value;
49+
50+
/** Must be non-null after {@link #getStringValue()} has been invoked. */
4051
private String stringValue;
4152

42-
/** Creates a new attribute belonging to the given node using its accessor. */
43-
public Attribute(Node parent, String name, MethodHandle handle, Method m) {
44-
this.parent = parent;
45-
this.name = name;
46-
this.handle = handle;
47-
this.method = m;
53+
/**
54+
* Creates a new attribute belonging to the given node using its accessor.
55+
*
56+
* @param handle A method handle, used to fetch the attribute.
57+
* @param method The method corresponding to the method handle. This
58+
* is used to perform reflective queries, eg to
59+
* find annotations on the attribute getter, but only
60+
* the method handle is ever invoked.
61+
*/
62+
public Attribute(@NonNull Node parent, @NonNull String name, @NonNull MethodHandle handle, @NonNull Method method) {
63+
this.parent = Objects.requireNonNull(parent);
64+
this.name = Objects.requireNonNull(name);
65+
this.handle = Objects.requireNonNull(handle);
66+
this.method = Objects.requireNonNull(method);
4867
}
4968

5069
/** Creates a new attribute belonging to the given node using its string value. */
51-
public Attribute(Node parent, String name, String value) {
52-
this.parent = parent;
53-
this.name = name;
70+
public Attribute(@NonNull Node parent, @NonNull String name, @Nullable String value) {
71+
this.parent = Objects.requireNonNull(parent);
72+
this.name = Objects.requireNonNull(name);
5473
this.value = value;
5574
this.handle = null;
5675
this.method = null;
57-
this.stringValue = value;
76+
this.stringValue = value == null ? "" : value;
5877
this.invoked = true;
5978
}
6079

6180

62-
6381
/**
6482
* Gets the generic type of the value of this attribute.
6583
*/
6684
public Type getType() {
6785
return method == null ? String.class : method.getGenericReturnType();
6886
}
6987

70-
public String getName() {
88+
/** Return the name of the attribute (without leading @ sign). */
89+
public @NonNull String getName() {
7190
return name;
7291
}
7392

7493

75-
public Node getParent() {
94+
/** Return the node that owns this attribute. */
95+
public @NonNull Node getParent() {
7696
return parent;
7797
}
7898

@@ -99,13 +119,23 @@ public String replacementIfDeprecated() {
99119
}
100120
}
101121

122+
/**
123+
* Return whether this attribute was deprecated. This is the case if the getter
124+
* has the annotation {@link Deprecated} or {@link DeprecatedAttribute}.
125+
*/
102126
public boolean isDeprecated() {
103127
return replacementIfDeprecated() != null;
104128
}
105129

130+
/**
131+
* Return the value of the attribute. This may return null. The getter
132+
* is invoked at most once.
133+
*/
106134
public Object getValue() {
107135
if (this.invoked) {
108136
return this.value;
137+
} else if (handle == null) {
138+
throw new NullPointerException("Cannot fetch value of attribute with null getter! " + this);
109139
}
110140

111141
Object value;
@@ -121,13 +151,23 @@ public Object getValue() {
121151
return value;
122152
}
123153

124-
public String getStringValue() {
154+
/**
155+
* Return the string value of the attribute. If the getter returned null,
156+
* then return the empty string (which is a falsy value in XPath).
157+
* Otherwise, return a string representation of the value (e.g. with
158+
* {@link Object#toString()}, but this is not guaranteed).
159+
*/
160+
public @NonNull String getStringValue() {
125161
if (stringValue != null) {
126162
return stringValue;
127163
}
128164
Object v = getValue();
129165

130-
stringValue = v == null ? "" : String.valueOf(v);
166+
if (v == null) {
167+
stringValue = "";
168+
} else {
169+
stringValue = v.toString();
170+
}
131171
return stringValue;
132172
}
133173

@@ -148,11 +188,11 @@ public boolean equals(Object o) {
148188

149189
@Override
150190
public int hashCode() {
151-
return Objects.hash(parent, name);
191+
return parent.hashCode() * 31 + name.hashCode();
152192
}
153193

154194
@Override
155195
public String toString() {
156-
return name + ':' + getValue() + ':' + parent.getXPathNodeName();
196+
return parent.getXPathNodeName() + "/@" + name + " = " + getValue();
157197
}
158198
}

pmd-core/src/main/java/net/sourceforge/pmd/lang/rule/xpath/impl/AttributeAxisIterator.java

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,28 @@
44

55
package net.sourceforge.pmd.lang.rule.xpath.impl;
66

7+
import static net.sourceforge.pmd.util.CollectionUtil.emptyList;
8+
import static net.sourceforge.pmd.util.CollectionUtil.setOf;
9+
710
import java.lang.invoke.MethodHandle;
811
import java.lang.invoke.MethodHandles;
912
import java.lang.invoke.MethodHandles.Lookup;
1013
import java.lang.invoke.MethodType;
1114
import java.lang.reflect.Method;
15+
import java.lang.reflect.Modifier;
1216
import java.util.Arrays;
13-
import java.util.HashSet;
1417
import java.util.Iterator;
1518
import java.util.List;
1619
import java.util.Set;
1720
import java.util.concurrent.ConcurrentHashMap;
1821
import java.util.concurrent.ConcurrentMap;
1922
import java.util.stream.Collectors;
2023

24+
import org.checkerframework.checker.nullness.qual.NonNull;
25+
2126
import net.sourceforge.pmd.lang.ast.Node;
2227
import net.sourceforge.pmd.lang.ast.impl.AbstractNode;
28+
import net.sourceforge.pmd.lang.document.Chars;
2329
import net.sourceforge.pmd.lang.rule.xpath.Attribute;
2430
import net.sourceforge.pmd.lang.rule.xpath.NoAttribute;
2531
import net.sourceforge.pmd.lang.rule.xpath.NoAttribute.NoAttrScope;
@@ -31,6 +37,8 @@
3137
* attributes. This is the default way the attributes of a node
3238
* are made accessible to XPath rules, and defines an important
3339
* piece of PMD's XPath support.
40+
*
41+
* @see Node#getXPathAttributesIterator()
3442
*/
3543
public class AttributeAxisIterator implements Iterator<Attribute> {
3644

@@ -39,25 +47,25 @@ public class AttributeAxisIterator implements Iterator<Attribute> {
3947

4048
/* Constants used to determine which methods are accessors */
4149
private static final Set<Class<?>> CONSIDERED_RETURN_TYPES
42-
= new HashSet<>(Arrays.<Class<?>>asList(Integer.TYPE, Boolean.TYPE, Double.TYPE, String.class,
43-
Long.TYPE, Character.TYPE, Float.TYPE));
50+
= setOf(Integer.TYPE, Boolean.TYPE, Double.TYPE, String.class,
51+
Long.TYPE, Character.TYPE, Float.TYPE, Chars.class);
4452

4553
private static final Set<String> FILTERED_OUT_NAMES
46-
= new HashSet<>(Arrays.asList("toString",
47-
"getNumChildren",
48-
"getIndexInParent",
49-
"getParent",
50-
"getClass",
51-
"getSourceCodeFile",
52-
"isFindBoundary",
53-
"getRuleIndex",
54-
"getXPathNodeName",
55-
"altNumber",
56-
"toStringTree",
57-
"getTypeNameNode",
58-
"hashCode",
59-
"getImportedNameNode",
60-
"getScope"));
54+
= setOf("toString",
55+
"getNumChildren",
56+
"getIndexInParent",
57+
"getParent",
58+
"getClass",
59+
"getSourceCodeFile",
60+
"isFindBoundary",
61+
"getRuleIndex",
62+
"getXPathNodeName",
63+
"altNumber",
64+
"toStringTree",
65+
"getTypeNameNode",
66+
"hashCode",
67+
"getImportedNameNode",
68+
"getScope");
6169

6270
/* Iteration variables */
6371
private final Iterator<MethodWrapper> iterator;
@@ -69,7 +77,7 @@ public class AttributeAxisIterator implements Iterator<Attribute> {
6977
* Note: if you want to access the attributes of a node, don't use this directly,
7078
* use instead the overridable {@link Node#getXPathAttributesIterator()}.
7179
*/
72-
public AttributeAxisIterator(Node contextNode) {
80+
public AttributeAxisIterator(@NonNull Node contextNode) {
7381
this.node = contextNode;
7482
this.iterator = METHOD_CACHE.computeIfAbsent(contextNode.getClass(), this::getWrappersForClass).iterator();
7583
}
@@ -79,8 +87,8 @@ private List<MethodWrapper> getWrappersForClass(Class<?> nodeClass) {
7987
.filter(m -> isAttributeAccessor(nodeClass, m))
8088
.map(m -> {
8189
try {
82-
return new MethodWrapper(m);
83-
} catch (IllegalAccessException e) {
90+
return new MethodWrapper(m, nodeClass);
91+
} catch (ReflectiveOperationException e) {
8492
throw AssertionUtil.shouldNotReachHere("Method should be accessible " + e);
8593
}
8694
})
@@ -104,6 +112,8 @@ && isConsideredReturnType(method)
104112
// filter out methods declared in supertypes like the
105113
// Antlr ones, unless they're opted-in
106114
&& Node.class.isAssignableFrom(method.getDeclaringClass())
115+
// Methods of package-private classes are not accessible.
116+
&& Modifier.isPublic(method.getModifiers())
107117
&& !isIgnored(nodeClass, method);
108118
}
109119

@@ -171,15 +181,24 @@ public boolean hasNext() {
171181
private static class MethodWrapper {
172182
static final Lookup LOOKUP = MethodHandles.publicLookup();
173183
private static final MethodType GETTER_TYPE = MethodType.methodType(Object.class, Node.class);
174-
public MethodHandle methodHandle;
175-
public Method method;
176-
public String name;
184+
public final MethodHandle methodHandle;
185+
public final Method method;
186+
public final String name;
177187

178188

179-
MethodWrapper(Method m) throws IllegalAccessException {
189+
MethodWrapper(Method m, Class<?> nodeClass) throws IllegalAccessException, NoSuchMethodException {
180190
this.method = m;
181-
this.methodHandle = LOOKUP.unreflect(m).asType(GETTER_TYPE);
182191
this.name = truncateMethodName(m.getName());
192+
193+
if (!Modifier.isPublic(m.getDeclaringClass().getModifiers())) {
194+
// This is a public method of a non-public class.
195+
// To call it from reflection we need to call it via invokevirtual,
196+
// whereas the default handle would use invokespecial.
197+
MethodType methodType = MethodType.methodType(m.getReturnType(), emptyList());
198+
this.methodHandle = MethodWrapper.LOOKUP.findVirtual(nodeClass, m.getName(), methodType).asType(GETTER_TYPE);
199+
} else {
200+
this.methodHandle = LOOKUP.unreflect(m).asType(GETTER_TYPE);
201+
}
183202
}
184203

185204

pmd-core/src/main/java/net/sourceforge/pmd/lang/rule/xpath/internal/DomainConversion.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ public static AtomicValue getAtomicRepresentation(final Object value) {
133133
if (value == null) {
134134
return UntypedAtomicValue.ZERO_LENGTH_UNTYPED;
135135

136-
} else if (value instanceof String) {
137-
return new StringValue((String) value);
136+
} else if (value instanceof CharSequence) {
137+
return new StringValue((CharSequence) value);
138138
} else if (value instanceof Boolean) {
139139
return BooleanValue.get((Boolean) value);
140140
} else if (value instanceof Integer) {

pmd-java/src/test/java/net/sourceforge/pmd/lang/java/ast/AllJavaAstTreeDumpTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
@Suite
1111
@SelectClasses({
1212
ParserCornersTest.class,
13+
Java9TreeDumpTest.class,
1314
Java14TreeDumpTest.class,
1415
Java15TreeDumpTest.class,
1516
Java16TreeDumpTest.class,

pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/Bug1429.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
+- InfixExpression[@CompileTimeConstant = false, @Operator = BinaryOp.EQ, @ParenthesisDepth = 0, @Parenthesized = false]
1919
| +- FieldAccess[@AccessType = AccessType.READ, @CompileTimeConstant = false, @Image = "attributes", @Name = "attributes", @ParenthesisDepth = 0, @Parenthesized = false]
2020
| | +- ThisExpression[@CompileTimeConstant = false, @ParenthesisDepth = 0, @Parenthesized = false]
21-
| +- NullLiteral[@CompileTimeConstant = false, @ParenthesisDepth = 0, @Parenthesized = false]
21+
| +- NullLiteral[@CompileTimeConstant = false, @LiteralText = "null", @ParenthesisDepth = 0, @Parenthesized = false]
2222
+- MethodCall[@CompileTimeConstant = false, @Image = "emptySet", @MethodName = "emptySet", @ParenthesisDepth = 0, @Parenthesized = false]
2323
| +- AmbiguousName[@CompileTimeConstant = false, @Image = "Collections", @Name = "Collections", @ParenthesisDepth = 0, @Parenthesized = false]
2424
| +- TypeArguments[@Diamond = false, @Empty = false, @Size = 1]
@@ -91,7 +91,7 @@
9191
| +- MethodCall[@CompileTimeConstant = false, @Image = "concat", @MethodName = "concat", @ParenthesisDepth = 0, @Parenthesized = false]
9292
| | +- VariableAccess[@AccessType = AccessType.READ, @CompileTimeConstant = false, @Image = "result", @Name = "result", @ParenthesisDepth = 0, @Parenthesized = false]
9393
| | +- ArgumentList[@Empty = false, @Size = 1]
94-
| | +- StringLiteral[@CompileTimeConstant = true, @ConstValue = ":", @Empty = false, @Image = "\":\"", @Length = 1, @ParenthesisDepth = 0, @Parenthesized = false, @TextBlock = false]
94+
| | +- StringLiteral[@CompileTimeConstant = true, @ConstValue = ":", @Empty = false, @Image = "\":\"", @Length = 1, @LiteralText = "\":\"", @ParenthesisDepth = 0, @Parenthesized = false, @TextBlock = false]
9595
| +- ArgumentList[@Empty = false, @Size = 1]
9696
| +- VariableAccess[@AccessType = AccessType.READ, @CompileTimeConstant = false, @Image = "value", @Name = "value", @ParenthesisDepth = 0, @Parenthesized = false]
9797
+- ReturnStatement[]

pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/ast/GitHubBug208.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@
2323
+- FormalParameters[@Empty = true, @Size = 0]
2424
+- Block[@Empty = false, @Size = 1, @containsComment = false]
2525
+- ReturnStatement[]
26-
+- NullLiteral[@CompileTimeConstant = false, @ParenthesisDepth = 0, @Parenthesized = false]
26+
+- NullLiteral[@CompileTimeConstant = false, @LiteralText = "null", @ParenthesisDepth = 0, @Parenthesized = false]

0 commit comments

Comments
 (0)