Merge branch 'xml-new-xpath-rule' into pmd7-merge-xml-rule

This commit is contained in:
Clément Fournier
2022-03-24 19:21:17 +01:00
14 changed files with 800 additions and 34 deletions

View File

@ -12,7 +12,7 @@ GEM
concurrent-ruby (1.1.9)
cork (0.3.0)
colored2 (~> 3.1)
danger (8.4.3)
danger (8.4.5)
claide (~> 1.0)
claide-plugins (>= 0.9.2)
colored2 (~> 3.1)
@ -26,7 +26,7 @@ GEM
octokit (~> 4.7)
terminal-table (>= 1, < 4)
differ (0.1.2)
et-orbi (1.2.6)
et-orbi (1.2.7)
tzinfo
faraday (1.10.0)
faraday-em_http (~> 1.0)
@ -62,7 +62,7 @@ GEM
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (5.1.0)
liquid (5.2.0)
logger-colors (1.0.0)
mini_portile2 (2.8.0)
multipart-post (2.1.1)

View File

@ -1,7 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (6.0.4.6)
activesupport (6.0.4.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@ -14,8 +14,7 @@ GEM
execjs
coffee-script-source (1.11.1)
colorator (1.1.0)
commonmarker (0.17.13)
ruby-enum (~> 0.5)
commonmarker (0.23.4)
concurrent-ruby (1.1.9)
dnsruby (1.61.9)
simpleidn (~> 0.1)
@ -52,12 +51,12 @@ GEM
ffi (1.15.5)
forwardable-extended (2.6.0)
gemoji (3.0.1)
github-pages (223)
github-pages (225)
github-pages-health-check (= 1.17.9)
jekyll (= 3.9.0)
jekyll-avatar (= 0.7.0)
jekyll-coffeescript (= 1.1.1)
jekyll-commonmark-ghpages (= 0.1.6)
jekyll-commonmark-ghpages (= 0.2.0)
jekyll-default-layout (= 0.1.4)
jekyll-feed (= 0.15.1)
jekyll-gist (= 1.5.0)
@ -71,7 +70,7 @@ GEM
jekyll-relative-links (= 0.6.1)
jekyll-remote-theme (= 0.4.3)
jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.7.1)
jekyll-seo-tag (= 2.8.0)
jekyll-sitemap (= 1.4.0)
jekyll-swiss (= 1.0.0)
jekyll-theme-architect (= 0.2.0)
@ -127,12 +126,12 @@ GEM
jekyll-coffeescript (1.1.1)
coffee-script (~> 2.2)
coffee-script-source (~> 1.11.1)
jekyll-commonmark (1.3.1)
commonmarker (~> 0.14)
jekyll (>= 3.7, < 5.0)
jekyll-commonmark-ghpages (0.1.6)
commonmarker (~> 0.17.6)
jekyll-commonmark (~> 1.2)
jekyll-commonmark (1.4.0)
commonmarker (~> 0.22)
jekyll-commonmark-ghpages (0.2.0)
commonmarker (~> 0.23.4)
jekyll (~> 3.9.0)
jekyll-commonmark (~> 1.4.0)
rouge (>= 2.0, < 4.0)
jekyll-default-layout (0.1.4)
jekyll (~> 3.0)
@ -164,7 +163,7 @@ GEM
rubyzip (>= 1.3.0, < 3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-seo-tag (2.7.1)
jekyll-seo-tag (2.8.0)
jekyll (>= 3.8, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
@ -248,8 +247,6 @@ GEM
ffi (~> 1.0)
rexml (3.2.5)
rouge (3.26.0)
ruby-enum (0.9.0)
i18n
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
safe_yaml (1.0.5)

View File

@ -367,6 +367,9 @@ entries:
- title: Visualforce
url: /pmd_languages_visualforce.html
output: web, pdf
- title: XML and XML dialects
url: /pmd_languages_xml.html
output: web, pdf
- title: Developer Documentation
output: web, pdf
folderitems:

View File

@ -7,24 +7,93 @@ aliases:
type: "xs:string"
description: "The qualified name of a Java class, possibly with pairs of brackets to indicate an array type.
Can also be a primitive type name."
- &node_param
name: element
type: "xs:element"
description: "Any element node"
- &needs_typenode "The context node must be a {% jdoc jast::TypeNode %}"
- &coord_fun_note |
The function is not context-dependent, but takes a node as its first parameter.
The function is only available in XPath 2.0.
- &needs_node_ctx "The requires the context node to be an element"
langs:
- name: "Any language"
- name: "All languages"
ns: "pmd"
header: "Functions available to all languages are in the namespace `pmd`."
funs:
- name: fileName
returnType: "xs:string"
shortDescription: "Returns the current filename"
description: "Returns the current simple filename without path but including the extension.
This can be used to write rules that check filename naming conventions.
<p>This function is available since PMD 6.38.0.</p>"
notes: "The function can be called on any node."
shortDescription: "Returns the simple name of the current file"
description: |
Returns the current simple file name, without path but including the extension.
This can be used to write rules that check file naming conventions.
since: 6.38.0
notes: *needs_node_ctx
examples:
- code: "//b[pmd:fileName() = 'Foo.xml']"
outcome: "Matches any `&lt;b&gt;` tags in files called `Foo.xml`."
- name: startLine
returnType: "xs:int"
parameters:
- *node_param
shortDescription: "Returns the start line of the given node"
description: |
Returns the line where the node starts in the source file.
Line numbers are 1-based.
since: 6.44.0
notes: *coord_fun_note
examples:
- code: "//b[pmd:startLine(.) > 5]"
outcome: "Matches any `&lt;b&gt;` node which starts after the fifth line."
- name: endLine
returnType: "xs:int"
parameters:
- *node_param
shortDescription: "Returns the end line of the given node"
description: |
Returns the line where the node ends in the source file.
Line numbers are 1-based.
since: 6.44.0
notes: *coord_fun_note
examples:
- code: "//b[pmd:endLine(.) == pmd:startLine(.)]"
outcome: "Matches any `&lt;b&gt;` node which doesn't span more than one line."
- name: startColumn
returnType: "xs:int"
parameters:
- *node_param
shortDescription: "Returns the start column of the given node (inclusive)"
description: |
Returns the column number where the node starts in the source file.
Column numbers are 1-based. The start column is inclusive.
since: 6.44.0
notes: *coord_fun_note
examples:
- code: "//b[pmd:startColumn(.) = 1]"
outcome: "Matches any `&lt;b&gt;` node which starts on the first column of a line"
- name: endColumn
returnType: "xs:int"
parameters:
- *node_param
shortDescription: "Returns the end column of the given node (exclusive)"
description: |
Returns the column number where the node ends in the source file.
Column numbers are 1-based. The end column is exclusive.
since: 6.44.0
notes: *coord_fun_note
examples:
- code: "//b[pmd:startLine(.) = pmd:endLine(.) and pmd:endColumn(.) - pmd:startColumn(.) = 1]"
outcome: "Matches any `&lt;b&gt;` node which spans exactly one character"
- name: "Java"
ns: "pmd-java"

View File

@ -4,7 +4,11 @@
### {{ lang.name }}
{% if lang.header %}
{{ lang.header | render_markdown }}
{% else %}
{{ lang.name }} functions are in the namespace `{{ lang.ns }}`.
{% endif %}
<div class="table-responsive">
<table width="100%">
@ -50,6 +54,10 @@
<dl>
<dd>{{ fun.description | render_markdown }}</dd>
{% if fun.since %}
<dt>Since</dt>
<dd>PMD {{ fun.since }}</dd>
{% endif %}
<dt>Remarks</dt>
<dd>{{ fun.notes | render_markdown }}</dd>

View File

@ -29,6 +29,8 @@ body {
margin-top: 60px;
margin-left: -15px;
margin-right: 15px;
height: 80%;
overflow: auto;
}
.container {
margin-left: 15px;
@ -49,6 +51,8 @@ body {
margin-top: 60px;
margin-left: -15px;
margin-right: 15px;
height: 80%;
overflow: auto;
}
.container {
margin-left: 15px;

View File

@ -0,0 +1,75 @@
---
title: Processing XML files
permalink: pmd_languages_xml.html
last_updated: March 2022 (6.44.0)
---
## The XML language module
PMD has an XML language module which exposes the [DOM](https://de.wikipedia.org/wiki/Document_Object_Model)
of an XML document as an AST. Different flavours of XML are represented by separate
language instances, which all use the same parser under the hood. The following
table lists the languages currently provided by the `pmd-xml` maven module.
| Language ID | Description |
|-------------|-----------------------------------|
| xml | Generic XML language |
| pom | Maven Project Object Model (POM) |
| wsdl | Web Services Description Language |
| xsl | Extensible Stylesheet Language |
Each of those languages has a separate rule index, and may provide domain-specific
[XPath functions](pmd_userdocs_extending_writing_xpath_rules.html#pmd-extension-functions).
At their core they use the same parsing facilities though.
### File attribution
Any file ending with `.xml` is associated with the `xml` language. Other XML flavours
use more specific extensions, like `.xsl`.
Some XML-based file formats do not conventionally use a `.xml` extension. To associate
these files with the XML language, you need to use the `--force-language xml` command-line
arguments, for instance:
```
$ ./run.sh pmd -d /home/me/src/xml-file.ext -f text -R ruleset.xml --force-language xml
```
Please refer to [PMD CLI reference](pmd_userdocs_cli_reference.html#analyze-other-xml-formats)
for more examples.
### XPath rules in XML
While other languages use {% jdoc core::lang.rule.XPathRule %} to create XPath rules,
the use of this class is not recommended for XML languages. Instead, since 6.44.0, you
are advised to use {% jdoc xml::lang.xml.rule.DomXPathRule %}. This rule class interprets
XPath queries exactly as regular XPath, while `XPathRule` works on a wrapper for the
DOM which is inconsistent with the XPath spec. Since `DomXPathRule` conforms to the
XPath spec, you can
- test XML queries in any stock XPath testing tool, or use resources like StackOverflow
to help you write XPath queries.
- match XML comments and processing instructions
- use standard XPath functions like `text()` or `fn:string`
{% include note.html content="The Rule Designer only works with `XPathRule`, and the tree it prints is inconsistent with the DOM representation used by `DomXPathRule`. You can use an online free XPath testing tool to test your query instead." %}
Here's an example declaration of a `DomXPathRule`:
```xml
<rule name="MyXPathRule"
language="xml"
message="A message"
class="net.sourceforge.pmd.lang.xml.rule.DomXPathRule">
<properties>
<property name="xpath">
<value><![CDATA[
/a/b/c[@attr = "5"]
]]></value>
</property>
<!-- Note: the property "version" is unsupported. -->
</properties>
</rule>
```
The most important change is the `class` attribute, which doesn't point to `XPathRule`
but to `DomXPathRule`. Please see the Javadoc for {% jdoc xml::lang.xml.rule.DomXPathRule %}
for more info about the differences with `XPathRule`.

View File

@ -60,6 +60,8 @@ The CLI itself remains compatible, if you run PMD via command-line, no action is
* [#3773](https://github.com/pmd/pmd/pull/3773): \[apex] EagerlyLoadedDescribeSObjectResult false positives with SObjectField.getDescribe()
* core
* [#3299](https://github.com/pmd/pmd/issues/3299): \[core] Deprecate system properties of PMDCommandLineInterface
* doc
* [#3812](https://github.com/pmd/pmd/issues/3812): \[doc] Documentation website table of contents broken on pages with many subheadings
### API Changes
@ -96,6 +98,7 @@ The CLI itself remains compatible, if you run PMD via command-line, no action is
### External Contributions
* [#3773](https://github.com/pmd/pmd/pull/3773): \[apex] EagerlyLoadedDescribeSObjectResult false positives with SObjectField.getDescribe() - [@filiprafalowicz](https://github.com/filiprafalowicz)
* [#3836](https://github.com/pmd/pmd/pull/3836): \[doc] Make TOC scrollable when too many subheadings - [@JerritEic](https://github.com/JerritEic)
{% endtocmaker %}

View File

@ -15,15 +15,15 @@ import net.sourceforge.pmd.lang.ast.impl.DummyTreeUtil;
public class RuleContextTest {
public static Report getReport(Rule rule, BiConsumer<Rule, RuleContext> sideEffects) throws Exception {
public static Report getReport(Rule rule, BiConsumer<Rule, RuleContext> sideEffects) {
return Report.buildReport(listener -> sideEffects.accept(rule, RuleContext.create(listener, rule)));
}
public static Report getReportForRuleApply(Rule rule, Node node) throws Exception {
public static Report getReportForRuleApply(Rule rule, Node node) {
return getReport(rule, (r, ctx) -> r.apply(node, ctx));
}
public static Report getReportForRuleSetApply(RuleSet ruleset, RootNode node) throws Exception {
public static Report getReportForRuleSetApply(RuleSet ruleset, RootNode node) {
return Report.buildReport(listener -> new RuleSets(ruleset).apply(node, listener));
}

View File

@ -5,6 +5,7 @@
package net.sourceforge.pmd.lang.rule;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import org.hamcrest.Matchers;
@ -14,6 +15,7 @@ import org.junit.contrib.java.lang.system.SystemErrRule;
import net.sourceforge.pmd.Report;
import net.sourceforge.pmd.RuleContextTest;
import net.sourceforge.pmd.lang.DummyLanguageModule;
import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.lang.ast.DummyNode;
import net.sourceforge.pmd.lang.ast.DummyNodeWithDeprecatedAttribute;
@ -91,11 +93,73 @@ public class XPathRuleTest {
return xpr;
}
public DummyNode newNode() {
public XPathRule makeXPath(String xpathExpr) {
XPathRule xpr = new XPathRule(XPathVersion.XPATH_2_0, xpathExpr);
xpr.setLanguage(LanguageRegistry.getLanguage(DummyLanguageModule.NAME));
xpr.setName("name");
xpr.setMessage("gotcha");
return xpr;
}
@Test
public void testFileNameInXpath() {
Report report = executeRule(makeXPath("//*[pmd:fileName() = 'Foo.cls']"),
newRoot("src/Foo.cls"));
assertThat(report.getViolations(), hasSize(1));
}
@Test
public void testBeginLine() {
Report report = executeRule(makeXPath("//*[pmd:startLine(.)=1]"),
newRoot("src/Foo.cls"));
assertThat(report.getViolations(), hasSize(1));
}
@Test
public void testBeginCol() {
Report report = executeRule(makeXPath("//*[pmd:startColumn(.)=1]"),
newRoot("src/Foo.cls"));
assertThat(report.getViolations(), hasSize(1));
}
@Test
public void testEndLine() {
Report report = executeRule(makeXPath("//*[pmd:endLine(.)=1]"),
newRoot("src/Foo.cls"));
assertThat(report.getViolations(), hasSize(1));
}
@Test
public void testEndColumn() {
Report report = executeRule(makeXPath("//*[pmd:endColumn(.)>1]"),
newRoot("src/Foo.cls"));
assertThat(report.getViolations(), hasSize(1));
}
public Report executeRule(net.sourceforge.pmd.Rule rule, DummyNode node) {
return RuleContextTest.getReportForRuleApply(rule, node);
}
public DummyRoot newNode() {
DummyRoot root = new DummyRoot();
DummyNode dummy = new DummyNodeWithDeprecatedAttribute(2);
dummy.setCoords(1, 1, 1, 2);
root.addChild(dummy, 0);
return root;
}
public DummyRoot newRoot(String fileName) {
DummyRoot dummy = new DummyRoot().withFileName(fileName);
dummy.setCoords(1, 1, 1, 2);
return dummy;
}
}

View File

@ -91,7 +91,7 @@ public final class XmlParserImpl {
private final AstInfo<RootXmlNode> astInfo;
RootXmlNode(XmlParserImpl parser, Node domNode, ParserTask task) {
RootXmlNode(XmlParserImpl parser, Document domNode, ParserTask task) {
super(parser, domNode);
this.astInfo = new AstInfo<>(task, this);
}
@ -100,6 +100,16 @@ public final class XmlParserImpl {
public AstInfo<RootXmlNode> getAstInfo() {
return astInfo;
}
@Override
public XmlNode wrap(Node domNode) {
return super.wrap(domNode);
}
@Override
public Document getNode() {
return (Document) super.getNode();
}
}
}

View File

@ -0,0 +1,163 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.xml.rule;
import java.util.List;
import net.sourceforge.pmd.RuleContext;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.rule.AbstractRule;
import net.sourceforge.pmd.lang.rule.XPathRule;
import net.sourceforge.pmd.lang.xml.ast.XmlParser.RootXmlNode;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
/**
* XPath rule that executes an expression on the DOM directly, and not
* on the PMD AST wrapper. The XPath expressions adheres to the XPath
* (2.0) spec, so they can be tested in any existing XPath testing tools
* instead of just the PMD designer (google "xpath test"). Usage of this
* class is strongly recommended over the standard {@link XPathRule}, which
* is mostly useful in other PMD languages.
*
* <h3>Differences with {@link XPathRule}</h3>
*
* This rule and {@link XPathRule} do not accept exactly the same queries,
* because {@link XPathRule} implements the XPath spec in an ad-hoc way.
* The main differences are:
* <ul>
* <li>{@link XPathRule} uses <i>elements</i> to represent text nodes.
* This is contrary to the XPath spec, in which element and text nodes
* are different kinds of nodes. To replace the query {@code //elt/text[@Image="abc"]},
* use the XPath function {@code text()}, eg {@code //elt[text()="abc"]}.
* <li>{@link XPathRule} adds additional attributes to each element
* (eg {@code @BeginLine} and {@code @Image}). These attributes are not
* XML attributes, so they are not accessible using DomXPathRule rule.
* Instead, use the XPath functions {@code pmd:startLine(node)}, {@code pmd:endLine(node)} and related.
* For instance, replace {@code //elt[@EndLine - @BeginLine > 10]} with
* {@code elt[pmd:endLine(.) - pmd:startLine(.) > 10]}.
* <li>{@link XPathRule} uses an element called {@code "document"} as the
* root node of every XML AST. This node does not have the correct node kind,
* as it's an element, not a document. To replace {@code /document/RootNode},
* use just {@code /RootNode}.
* <li>{@link XPathRule} ignores comments and processing instructions
* (eg FXML's {@code <?import javafx.Node ?>}).
* This rule makes them accessible with the regular XPath syntax.
* The following finds all comments in the file:
* <pre>{@code
* //comment()
* }</pre>
* The following finds only top-level comments starting with "prefix":
* <pre>{@code
* /comment()[fn:starts-with(fn:string(.), "prefix")]
* }</pre>
* Note the use of {@code fn:string}.
*
* As an example of matching processing instructions, the following
* fetches all {@code <?import ... ?>} processing instructions.
* <pre>{@code
* /processing-instruction('import')
* }</pre>
* The string value of the instruction can be found with {@code fn:string}.
* </li>
* </ul>
*
* <p>Additionally, this rule only supports XPath 2.0, with no option
* for configuration. This will be bumped to XPath 3.1 in PMD 7.
*
* <h4>Namespace-sensitivity</h4>
*
* <p>Another important difference is that this rule is namespace-sensitive.
* If the tested XML documents use a schema ({@code xmlns} attribute on the root),
* you should set the property {@code defaultNsUri} on the rule with
* the value of the {@code xmlns} attribute. Otherwise node tests won't
* match unless you use a wildcard URI prefix ({@code *:nodeName}).
*
* <p>For instance for the document
* <pre>{@code
* <foo xmlns="http://company.com/aschema">
* </foo>
* }</pre>
* the XPath query {@code //foo} will not match anything, while {@code //*:foo}
* will. If you set the property {@code defaultNsUri} to {@code "http://company.com/aschema"},
* then {@code //foo} will be expanded to {@code //Q{http://company.com/aschema}foo},
* and match the {@code foo} node. The behaviour is equivalent in the following
* document:
* <pre>{@code
* <my:foo xmlns:my='http://company.com/aschema'>
* </my:foo>
* }</pre>
*
* <p>However, for the document
* <pre>{@code
* <foo>
* </foo>
* }</pre>
* the XPath queries {@code //foo} and {@code //*:foo} both match, because
* {@code //foo} is expanded to {@code //Q{}foo} (local name foo, empty URI),
* and the document has no default namespace (= the empty default namespace).
*
* <p>Note that explicitly specifying URIs with {@code Q{...}localName}
* as in this documentation is XPath 3.1 syntax and will only be available
* in PMD 7.
*
* @since PMD 6.44.0
* @author Clément Fournier
*/
public class DomXPathRule extends AbstractRule {
SaxonDomXPathQuery query;
private static final PropertyDescriptor<String> XPATH_EXPR
= PropertyFactory.stringProperty("xpath")
.desc("An XPath 2.0 expression that will be evaluated against the root DOM")
.defaultValue("") // no default value
.build();
private static final PropertyDescriptor<String> DEFAULT_NS_URI
= PropertyFactory.stringProperty("defaultNsUri")
.desc("A URI for the default namespace of node tests in the XPath expression."
+ "This is provided to match documents based on their declared schema.")
.defaultValue("")
.build();
public DomXPathRule() {
definePropertyDescriptor(XPATH_EXPR);
definePropertyDescriptor(DEFAULT_NS_URI);
}
public DomXPathRule(String xpath) {
this(xpath, "");
}
public DomXPathRule(String xpath, String defaultNsUri) {
this();
setProperty(XPATH_EXPR, xpath);
setProperty(DEFAULT_NS_URI, defaultNsUri);
}
@Override
public void apply(List<? extends Node> nodes, RuleContext ctx) {
for (Node n : nodes) {
RootXmlNode root = (RootXmlNode) n;
SaxonDomXPathQuery query = getXPathQuery();
for (Node foundNode : query.evaluate(root, this)) {
ctx.addViolation(foundNode);
}
}
}
private SaxonDomXPathQuery getXPathQuery() {
if (query == null) {
query = new SaxonDomXPathQuery(getProperty(XPATH_EXPR),
getProperty(DEFAULT_NS_URI),
getPropertyDescriptors());
}
return query;
}
}

View File

@ -0,0 +1,191 @@
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.lang.xml.rule;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import org.apache.commons.lang3.exception.ContextedRuntimeException;
import org.w3c.dom.Document;
import net.sourceforge.pmd.lang.ast.Node;
import net.sourceforge.pmd.lang.rule.xpath.SaxonXPathRuleQuery;
import net.sourceforge.pmd.lang.xml.ast.XmlNode;
import net.sourceforge.pmd.lang.xml.ast.XmlParser.RootXmlNode;
import net.sourceforge.pmd.lang.xpath.Initializer;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertySource;
import net.sourceforge.pmd.util.DataMap;
import net.sourceforge.pmd.util.DataMap.DataKey;
import net.sourceforge.pmd.util.DataMap.SimpleDataKey;
import net.sf.saxon.Configuration;
import net.sf.saxon.dom.DocumentWrapper;
import net.sf.saxon.dom.NodeWrapper;
import net.sf.saxon.om.Item;
import net.sf.saxon.om.NamePool;
import net.sf.saxon.om.NamespaceConstant;
import net.sf.saxon.om.ValueRepresentation;
import net.sf.saxon.sxpath.IndependentContext;
import net.sf.saxon.sxpath.XPathDynamicContext;
import net.sf.saxon.sxpath.XPathEvaluator;
import net.sf.saxon.sxpath.XPathExpression;
import net.sf.saxon.sxpath.XPathStaticContext;
import net.sf.saxon.sxpath.XPathVariable;
import net.sf.saxon.trans.XPathException;
final class SaxonDomXPathQuery {
private static final NamePool NAME_POOL = new NamePool();
private static final SimpleDataKey<DocumentWrapper> SAXON_DOM_WRAPPER
= DataMap.simpleDataKey("pmd.saxon.dom.wrapper");
/** The XPath expression as a string. */
private final String xpath;
/** The executable XPath expression. */
private final XPathExpressionWithProperties xpathExpression;
private final Configuration configuration;
SaxonDomXPathQuery(String xpath, String defaultNsUri, List<PropertyDescriptor<?>> properties) {
this.xpath = xpath;
configuration = new Configuration();
configuration.setNamePool(NAME_POOL);
xpathExpression = makeXPathExpression(this.xpath, defaultNsUri, properties);
}
private XPathExpressionWithProperties makeXPathExpression(String xpath, String defaultUri, List<PropertyDescriptor<?>> properties) {
final IndependentContext xpathStaticContext = new IndependentContext(configuration);
xpathStaticContext.declareNamespace("fn", NamespaceConstant.FN);
xpathStaticContext.setDefaultElementNamespace(defaultUri);
// Register PMD functions
Initializer.initialize(xpathStaticContext);
Map<PropertyDescriptor<?>, XPathVariable> xpathVariables = declareXPathVariables(properties, xpathStaticContext);
try {
final XPathEvaluator xpathEvaluator = new XPathEvaluator(configuration);
xpathEvaluator.setStaticContext(xpathStaticContext);
XPathExpression expression = xpathEvaluator.createExpression(xpath);
return new XPathExpressionWithProperties(
expression,
xpathVariables
);
} catch (final XPathException e) {
throw new ContextedRuntimeException(e)
.addContextValue("XPath", xpath);
}
}
private Map<PropertyDescriptor<?>, XPathVariable> declareXPathVariables(List<PropertyDescriptor<?>> accessibleProperties, XPathStaticContext xpathStaticContext) {
Map<PropertyDescriptor<?>, XPathVariable> xpathVariables = new HashMap<>();
for (final PropertyDescriptor<?> propertyDescriptor : accessibleProperties) {
final String name = propertyDescriptor.name();
if (!isExcludedProperty(name)) {
final XPathVariable xpathVariable = xpathStaticContext.declareVariable(null, name);
xpathVariables.put(propertyDescriptor, xpathVariable);
}
}
return Collections.unmodifiableMap(xpathVariables);
}
private boolean isExcludedProperty(String name) {
return "xpath".equals(name)
|| "defaultNsUri".equals(name)
|| "violationSuppressRegex".equals(name)
|| "violationSuppressXPath".equals(name);
}
@Override
public String toString() {
return xpath;
}
public List<Node> evaluate(RootXmlNode root, PropertySource propertyValues) {
DocumentWrapper wrapper = getSaxonDomWrapper(root);
try {
List<Node> result = new ArrayList<>();
for (Item item : this.xpathExpression.evaluate(wrapper, propertyValues)) {
if (item instanceof NodeWrapper) {
NodeWrapper nodeInfo = (NodeWrapper) item;
Object domNode = nodeInfo.getUnderlyingNode();
if (domNode instanceof org.w3c.dom.Node) {
XmlNode wrapped = root.wrap((org.w3c.dom.Node) domNode);
result.add(wrapped);
}
}
}
return result;
} catch (XPathException e) {
throw new ContextedRuntimeException(e)
.addContextValue("XPath", xpath);
}
}
private DocumentWrapper getSaxonDomWrapper(RootXmlNode node) {
DataMap<DataKey<?, ?>> userMap = node.getUserMap();
if (userMap.isSet(SAXON_DOM_WRAPPER)) {
return userMap.get(SAXON_DOM_WRAPPER);
}
Document domRoot = node.getNode();
DocumentWrapper wrapper = new DocumentWrapper(
domRoot, domRoot.getBaseURI(), configuration
);
userMap.set(SAXON_DOM_WRAPPER, wrapper);
return wrapper;
}
static final class XPathExpressionWithProperties {
final XPathExpression expr;
final Map<PropertyDescriptor<?>, XPathVariable> xpathVariables;
XPathExpressionWithProperties(XPathExpression expr, Map<PropertyDescriptor<?>, XPathVariable> xpathVariables) {
this.expr = expr;
this.xpathVariables = xpathVariables;
}
@SuppressWarnings("unchecked")
private List<Item> evaluate(final DocumentWrapper elementNode, PropertySource properties) throws XPathException {
XPathDynamicContext dynamicContext = createDynamicContext(elementNode, properties);
return (List<Item>) expr.evaluate(dynamicContext);
}
private XPathDynamicContext createDynamicContext(final DocumentWrapper elementNode, PropertySource properties) {
final XPathDynamicContext dynamicContext = expr.createDynamicContext(elementNode);
// Set variable values on the dynamic context
for (final Entry<PropertyDescriptor<?>, XPathVariable> entry : xpathVariables.entrySet()) {
ValueRepresentation saxonValue = getSaxonValue(properties, entry);
XPathVariable variable = entry.getValue();
try {
dynamicContext.setVariable(variable, saxonValue);
} catch (XPathException e) {
throw new ContextedRuntimeException(e)
.addContextValue("Variable", variable);
}
}
return dynamicContext;
}
private static ValueRepresentation getSaxonValue(PropertySource properties, Entry<PropertyDescriptor<?>, XPathVariable> entry) {
Object value = properties.getProperty(entry.getKey());
Objects.requireNonNull(value, "null property value for " + entry.getKey());
return SaxonXPathRuleQuery.getRepresentation(entry.getKey(), value);
}
}
}

View File

@ -9,18 +9,33 @@ import static net.sourceforge.pmd.lang.ast.test.TestUtilsKt.assertSize;
import org.junit.Test;
import net.sourceforge.pmd.Report;
import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.lang.rule.XPathRule;
import net.sourceforge.pmd.lang.rule.xpath.XPathVersion;
import net.sourceforge.pmd.lang.xml.XmlLanguageModule;
import net.sourceforge.pmd.lang.xml.XmlParsingHelper;
public class XmlXPathRuleTest {
private static final String A_URI = "http://soap.sforce.com/2006/04/metadata";
private static final String FXML_IMPORTS = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "\n"
+ "<!--suppress JavaFxDefaultTag -->\n"
+ "\n"
+ "<?import javafx.scene.layout.AnchorPane?>\n"
+ "<?import javafx.scene.layout.BorderPane?>\n"
+ "<?import javafx.scene.control.Tooltip?>\n"
+ "<?import javafx.scene.control.Label?>\n"
+ "<?import org.kordamp.ikonli.javafx.FontIcon?>\n"
+ "<AnchorPane prefHeight=\"750.0\" prefWidth=\"1200.0\" stylesheets=\"@../css/designer.css\" xmlns=\"http://javafx.com/javafx/8\" xmlns:fx=\"http://javafx.com/fxml/1\">\n"
+ "</AnchorPane>";
final XmlParsingHelper xml = XmlParsingHelper.XML;
private XPathRule makeXPath(String expression) {
XPathRule rule = new XPathRule(XPathVersion.XPATH_2_0, expression);
private Rule makeXPath(String expression) {
return makeXPath(expression, "");
}
private Rule makeXPath(String expression, String nsUri) {
DomXPathRule rule = new DomXPathRule(expression, nsUri);
rule.setLanguage(LanguageRegistry.getLanguage(XmlLanguageModule.NAME));
rule.setMessage("XPath Rule Failed");
return rule;
@ -36,4 +51,168 @@ public class XmlXPathRuleTest {
assertSize(report, 1);
}
@Test
public void testTextFunctionInXpath() {
// https://github.com/pmd/pmd/issues/915
Report report = xml.executeRule(makeXPath("//app[text()[1]='app2']"),
"<a><app>app2</app></a>");
assertSize(report, 1);
}
@Test
public void testRootNodeWildcardUri() {
// https://github.com/pmd/pmd/issues/3413#issuecomment-1072614398
Report report = xml.executeRule(makeXPath("/*:Flow"),
"<Flow xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n"
+ "</Flow>");
assertSize(report, 1);
}
@Test
public void testNoNamespaceRoot() {
Report report = xml.executeRule(makeXPath("/Flow"),
"<Flow>\n"
+ "</Flow>");
assertSize(report, 1);
}
@Test
public void testNamespaceDescendantWrongDefaultUri() {
Report report = xml.executeRule(makeXPath("//a"),
"<Flow xmlns='" + A_URI + "'><a/></Flow>");
assertSize(report, 0);
}
@Test
public void testNamespaceDescendantOkUri() {
Report report = xml.executeRule(makeXPath("//a", A_URI),
"<Flow xmlns='" + A_URI + "'><a/></Flow>");
assertSize(report, 1);
report = xml.executeRule(makeXPath("//*:a"),
"<Flow xmlns='" + A_URI + "'><a/></Flow>");
assertSize(report, 1);
}
@Test
public void testNamespaceDescendantWildcardUri() {
Report report = xml.executeRule(makeXPath("//*:a"),
"<Flow xmlns='" + A_URI + "'><a/></Flow>");
assertSize(report, 1);
}
@Test
public void testNamespacePrefixDescendantWildcardUri() {
Report report = xml.executeRule(makeXPath("//*:Flow"),
"<my:Flow xmlns:my='" + A_URI + "'><a/></my:Flow>");
assertSize(report, 1);
}
@Test
public void testNamespacePrefixDescendantOkUri() {
Report report = xml.executeRule(makeXPath("//Flow", A_URI),
"<my:Flow xmlns:my='" + A_URI + "'><a/></my:Flow>");
assertSize(report, 1);
}
@Test
public void testNamespacePrefixDescendantWrongUri() {
Report report = xml.executeRule(makeXPath("//Flow", "wrongURI"),
"<my:Flow xmlns:my='" + A_URI + "'><a/></my:Flow>");
assertSize(report, 0);
}
@Test
public void testRootExpr() {
Report report = xml.executeRule(makeXPath("/"),
"<Flow><a/></Flow>");
assertSize(report, 1);
}
@Test
public void testProcessingInstructions() {
Report report = xml.executeRule(makeXPath("/child::processing-instruction()", "http://javafx.com/javafx/8"),
FXML_IMPORTS);
assertSize(report, 5);
}
@Test
public void testProcessingInstructionsNamed() {
Report report = xml.executeRule(makeXPath("/child::processing-instruction('import')"),
FXML_IMPORTS);
assertSize(report, 5);
}
@Test
public void testProcessingInstructionXML() {
// <?xml ?> does not create a PI
Report report = xml.executeRule(makeXPath("/child::processing-instruction('xml')", "http://javafx.com/javafx/8"),
FXML_IMPORTS);
assertSize(report, 0);
}
@Test
public void testComments() {
Report report = xml.executeRule(makeXPath("/child::comment()[fn:starts-with(fn:string(.), 'suppress')]"),
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<!--suppress JavaFxDefaultTag -->\n"
+ "<AnchorPane prefHeight=\"750.0\" prefWidth=\"1200.0\" stylesheets=\"@../css/designer.css\" xmlns=\"http://javafx.com/javafx/8\" xmlns:fx=\"http://javafx.com/fxml/1\">\n"
+ "</AnchorPane>");
assertSize(report, 1);
}
@Test
public void testXmlNsFunctions() {
// https://github.com/pmd/pmd/issues/2766
Report report = xml.executeRule(
makeXPath("/manifest[namespace-uri-for-prefix('android', .) = 'http://schemas.android.com/apk/res/android']"),
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
+ " package=\"com.a.b\">\n"
+ "\n"
+ " <application\n"
+ " android:allowBackup=\"true\"\n"
+ " android:icon=\"@mipmap/ic_launcher\"\n"
+ " android:label=\"@string/app_name\"\n"
+ " android:roundIcon=\"@mipmap/ic_launcher_round\"\n"
+ " android:supportsRtl=\"true\"\n"
+ " android:theme=\"@style/AppTheme\">\n"
+ " <activity android:name=\".MainActivity\">\n"
+ " <intent-filter>\n"
+ " <action android:name=\"android.intent.action.MAIN\" />\n"
+ "\n"
+ " <category android:name=\"android.intent.category.LAUNCHER\" />\n"
+ " </intent-filter>\n"
+ " </activity>\n"
+ " </application>\n"
+ "\n"
+ "</manifest>");
assertSize(report, 1);
}
@Test
public void testLocationFuns() {
Rule rule = makeXPath("//Flow[pmd:startLine(.) != pmd:endLine(.)]");
Report report = xml.executeRule(rule, "<Flow><a/></Flow>");
assertSize(report, 0);
report = xml.executeRule(rule, "<Flow>\n<a/>\n</Flow>");
assertSize(report, 1);
}
}