Most of the work to extract module

Need a release of nice-xml-messages
Need an update to pmd-test
Need to add the 'ignored' attribute to schema
This commit is contained in:
Clément Fournier 2022-05-17 23:32:57 +02:00
parent 9d23d79802
commit 60139c8f4a
No known key found for this signature in database
GPG Key ID: 4D8D42402E4F47E2
10 changed files with 707 additions and 0 deletions

39
pmd-test-schema/pom.xml Normal file
View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>pmd-test-schema</artifactId>
<name>PMD Test Schema</name>
<description>
Parser for the XML test description format. The module has no dependency
on junit or other test-only dependencies.
</description>
<parent>
<artifactId>pmd</artifactId>
<groupId>net.sourceforge.pmd</groupId>
<version>6.46.0-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-core</artifactId>
</dependency>
<dependency>
<groupId>com.github.oowekyala.ooxml</groupId>
<artifactId>nice-xml-messages</artifactId>
<version>2.0</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,277 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.test.schema;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Stream;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.lang.LanguageRegistry;
import net.sourceforge.pmd.lang.LanguageVersion;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertySource;
import net.sourceforge.pmd.test.schema.TestSchemaParser.PmdXmlReporter;
import com.github.oowekyala.ooxml.DomUtils;
/**
* @author Clément Fournier
*/
class BaseTestParserImpl {
static class ParserV1 extends BaseTestParserImpl {
}
public TestCollection parseDocument(Rule rule, Document doc, PmdXmlReporter err) {
Element root = doc.getDocumentElement();
Map<String, Element> codeFragments = parseCodeFragments(err, root);
Set<String> usedFragments = new HashSet<>();
NodeList testCodes = root.getElementsByTagName("test-code");
TestCollection result = new TestCollection();
for (int i = 0; i < testCodes.getLength(); i++) {
TestDescriptor descriptor = new TestDescriptor(i, rule.deepCopy());
PmdXmlReporter errScope = err.newScope();
parseSingleTest((Element) testCodes.item(i), descriptor, codeFragments, usedFragments, errScope);
if (!errScope.hasError()) {
result.addTest(descriptor);
}
}
codeFragments.keySet().removeAll(usedFragments);
codeFragments.forEach((id, node) -> err.at(node).warn("Unused code fragment"));
return result;
}
private Map<String, Element> parseCodeFragments(PmdXmlReporter err, Element root) {
Map<String, Element> codeFragments = new HashMap<>();
for (Node node : DomUtils.asList(root.getElementsByTagName("code-fragment"))) {
Attr id = getRequiredAttribute("id", (Element) node, err);
if (id == null) {
continue;
}
Element prev = codeFragments.put(id.getValue(), (Element) node);
if (prev != null) {
err.at(prev).error("Fragment with duplicate id ''{0}'' is ignored", id.getValue());
}
}
return codeFragments;
}
private void parseSingleTest(Element testCode, TestDescriptor descriptor, Map<String, Element> fragments, Set<String> usedFragments, PmdXmlReporter err) {
{
String description = getSingleChildText(testCode, "description", true, err);
if (description == null) {
return;
}
descriptor.setDescription(description);
}
parseBoolAttribute(testCode, "reinitializeRule", true, err, "Attribute 'reinitializeRule' is deprecated and ignored, assumed true");
parseBoolAttribute(testCode, "useAuxClasspath", true, err, "Attribute 'useAuxClasspath' is deprecated and ignored, assumed true");
boolean ignored = parseBoolAttribute(testCode, "ignored", false, err, null)
&& !parseBoolAttribute(testCode, "regressionTest", true, err, "Attribute ''regressionTest'' is deprecated, use ''ignored'' with inverted value");
descriptor.setIgnored(ignored);
Properties properties = parseRuleProperties(testCode, descriptor.getRule(), err);
descriptor.getProperties().putAll(properties);
parseExpectedProblems(testCode, descriptor, err);
String code = getTestCode(testCode, fragments, err);
if (code == null) {
return;
}
descriptor.setCode(code);
LanguageVersion lversion = parseLanguageVersion(testCode, err);
descriptor.setLanguageVersion(lversion);
}
private void parseExpectedProblems(Element testCode, TestDescriptor descriptor, PmdXmlReporter err) {
Node expectedProblemsNode = getSingleChild(testCode, "expected-problems", true, err);
if (expectedProblemsNode == null) {
return;
}
int expectedProblems = Integer.parseInt(parseTextNode(expectedProblemsNode));
List<String> expectedMessages = null;
{
Element messagesNode = getSingleChild(testCode, "expected-messages", false, err);
if (messagesNode != null) {
expectedMessages = new ArrayList<>();
List<Node> messageNodes = DomUtils.asList(messagesNode.getElementsByTagName("message"));
if (messageNodes.size() != expectedProblems) {
err.at(expectedProblemsNode).error("Number of ''expected-messages'' ({0}) does not match", messageNodes.size());
return;
}
for (Node message : messageNodes) {
expectedMessages.add(parseTextNode(message));
}
}
}
List<Integer> expectedLineNumbers = null;
{
Element lineNumbers = getSingleChild(testCode, "expected-linenumbers", false, err);
if (lineNumbers != null) {
expectedLineNumbers = new ArrayList<>();
String[] linenos = parseTextNode(lineNumbers).split(",");
if (linenos.length != expectedProblems) {
err.at(expectedProblemsNode).error("Number of ''expected-linenumbers'' ({0}) does not match", linenos.length);
return;
}
for (String num : linenos) {
expectedLineNumbers.add(Integer.valueOf(num.trim()));
}
}
}
descriptor.recordExpectedViolations(
expectedProblems,
expectedLineNumbers,
expectedMessages
);
}
private String getTestCode(Element testCode, Map<String, Element> fragments, PmdXmlReporter err) {
String code = getSingleChildText(testCode, "code", false, err);
if (code == null) {
// Should have a coderef
NodeList coderefs = testCode.getElementsByTagName("code-ref");
if (coderefs.getLength() == 0) {
throw new RuntimeException(
"Required tag is missing from the test-xml. Supply either a code or a code-ref tag");
}
Element coderef = (Element) coderefs.item(0);
Attr id = getRequiredAttribute("id", coderef, err);
if (id == null) {
return null;
}
Element fragment = fragments.get(id.getValue());
if (fragment == null) {
err.at(id).error("Unknown id, known IDs are {0}", fragments.keySet());
return null;
}
code = parseTextNodeNoTrim(fragment);
}
return code;
}
private LanguageVersion parseLanguageVersion(Element testCode, PmdXmlReporter err) {
Node sourceTypeNode = getSingleChild(testCode, "source-type", false, err);
if (sourceTypeNode == null) {
return null;
}
String languageVersionString = parseTextNode(sourceTypeNode);
LanguageVersion languageVersion = LanguageRegistry.findLanguageVersionByTerseName(languageVersionString);
if (languageVersion != null) {
return languageVersion;
}
err.at(sourceTypeNode).error("Unknown language version ''{0}''", languageVersionString);
return null;
}
private Properties parseRuleProperties(Element testCode, PropertySource knownProps, PmdXmlReporter err) {
Properties properties = new Properties();
for (Node ruleProperty : DomUtils.asList(testCode.getElementsByTagName("rule-property"))) {
Node nameAttr = getRequiredAttribute("name", (Element) ruleProperty, err);
if (nameAttr == null) {
continue;
}
String propertyName = nameAttr.getNodeValue();
if (knownProps.getPropertyDescriptor(propertyName) == null) {
Stream<String> knownNames = knownProps.getPropertyDescriptors().stream().map(PropertyDescriptor::name);
err.at(nameAttr).error("Unknown property, known property names are {0}", knownNames);
continue;
}
properties.setProperty(propertyName, parseTextNode(ruleProperty));
}
return properties;
}
private Attr getRequiredAttribute(String name, Element ruleProperty, PmdXmlReporter err) {
Attr nameAttr = (Attr) ruleProperty.getAttributes().getNamedItem(name);
if (nameAttr == null) {
err.at(ruleProperty).error("Missing ''{0}'' attribute", name);
return null;
}
return nameAttr;
}
private boolean parseBoolAttribute(Element testCode, String attrName, boolean defaultValue, PmdXmlReporter err, String deprecationMessage) {
Attr attrNode = testCode.getAttributeNode(attrName);
if (attrNode != null) {
if (deprecationMessage != null) {
err.at(attrNode).warn(deprecationMessage);
}
return Boolean.parseBoolean(attrNode.getNodeValue());
}
return defaultValue;
}
private String getSingleChildText(Element parentElm, String nodeName, boolean required, PmdXmlReporter err) {
Node node = getSingleChild(parentElm, nodeName, required, err);
if (node == null) {
return null;
}
return parseTextNode(node);
}
private Element getSingleChild(Element parentElm, String nodeName, boolean required, PmdXmlReporter err) {
NodeList nodes = parentElm.getElementsByTagName(nodeName);
if (nodes.getLength() == 0) {
if (required) {
err.at(parentElm).error("Required child ''{0}'' is missing", nodeName);
}
return null;
} else if (nodes.getLength() > 1) {
err.at(nodes.item(1)).error("Duplicate tag ''{0}'' is ignored", nodeName);
}
return (Element) nodes.item(0);
}
private static String parseTextNode(Node exampleNode) {
return parseTextNodeNoTrim(exampleNode).trim();
}
private static String parseTextNodeNoTrim(Node exampleNode) {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < exampleNode.getChildNodes().getLength(); i++) {
Node node = exampleNode.getChildNodes().item(i);
if (node.getNodeType() == Node.CDATA_SECTION_NODE || node.getNodeType() == Node.TEXT_NODE) {
buffer.append(node.getNodeValue());
}
}
return buffer.toString();
}
}

View File

@ -0,0 +1,22 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.test.schema;
import java.util.List;
import java.util.Objects;
/**
* @author Clément Fournier
*/
public class TestCollection {
private List<TestDescriptor> tests;
public void addTest(TestDescriptor descriptor) {
tests.add(Objects.requireNonNull(descriptor));
}
}

View File

@ -0,0 +1,79 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.test.schema;
import java.util.List;
import java.util.Properties;
import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.lang.LanguageVersion;
/**
* @author Clément Fournier
*/
public class TestDescriptor {
private boolean ignored;
private String description;
private LanguageVersion languageVersion;
private Properties properties = new Properties();
private int index;
private final Rule rule;
private String code;
private int expectedProblems;
private List<Integer> expectedLineNumbers;
private List<String> expectedMessages;
public TestDescriptor(int index, Rule rule) {
this.index = index;
this.rule = rule;
}
public Rule getRule() {
return rule;
}
public Properties getProperties() {
return properties;
}
public boolean isIgnored() {
return ignored;
}
public void setIgnored(boolean ignored) {
this.ignored = ignored;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public LanguageVersion getLanguageVersion() {
return languageVersion;
}
public void setLanguageVersion(LanguageVersion languageVersion) {
this.languageVersion = languageVersion;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public void recordExpectedViolations(int expectedProblems, List<Integer> expectedLineNumbers, List<String> expectedMessages) {
this.expectedProblems = expectedProblems;
this.expectedLineNumbers = expectedLineNumbers;
this.expectedMessages = expectedMessages;
}
}

View File

@ -0,0 +1,163 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.test.schema;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.function.Consumer;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.InputSource;
import net.sourceforge.pmd.Rule;
import net.sourceforge.pmd.annotation.Experimental;
import com.github.oowekyala.ooxml.messages.NiceXmlMessageSpec;
import com.github.oowekyala.ooxml.messages.OoxmlFacade;
import com.github.oowekyala.ooxml.messages.PositionedXmlDoc;
import com.github.oowekyala.ooxml.messages.XmlException;
import com.github.oowekyala.ooxml.messages.XmlMessageReporter;
import com.github.oowekyala.ooxml.messages.XmlMessageReporterBase;
import com.github.oowekyala.ooxml.messages.XmlPosition;
import com.github.oowekyala.ooxml.messages.XmlPositioner;
import com.github.oowekyala.ooxml.messages.XmlSeverity;
/**
* Entry point to parse a test file.
*
* @author Clément Fournier
*/
@Experimental
public class TestSchemaParser {
private final TestSchemaVersion version;
public TestSchemaParser(TestSchemaVersion version) {
this.version = version;
}
/**
* Entry point to parse a test file.
*
* @param rule Rule which owns the tests
* @param inputSource Where to access the test file to parse
*
* @return A test collection, possibly incomplete
*
* @throws IOException If parsing throws this
* @throws XmlException If parsing throws this
*/
public TestCollection parse(Rule rule, InputSource inputSource) throws IOException, XmlException {
OoxmlFacade ooxml = new OoxmlFacade();
PositionedXmlDoc doc = ooxml.parse(newDocumentBuilder(), inputSource);
try (PmdXmlReporterImpl err = new PmdXmlReporterImpl(ooxml, doc.getPositioner())) {
TestCollection collection = version.getParserImpl().parseDocument(rule, doc.getDocument(), err);
if (err.hasError()) {
// todo maybe add a way not to throw here
throw new IllegalStateException("Errors were encountered while parsing XML tests");
}
return collection;
}
}
interface PmdXmlReporter extends XmlMessageReporter<Reporter> {
boolean hasError();
PmdXmlReporter newScope();
}
private static class PmdXmlReporterImpl
extends XmlMessageReporterBase<Reporter>
implements PmdXmlReporter {
private boolean hasError;
protected PmdXmlReporterImpl(OoxmlFacade ooxml,
XmlPositioner positioner) {
super(ooxml, positioner);
}
@Override
protected Reporter create2ndStage(XmlPosition position, XmlPositioner positioner, Consumer<XmlException> handleEx) {
return new Reporter(position, positioner, ooxml, handleEx.andThen(this::consumeEx));
}
protected void consumeEx(XmlException e) {
hasError |= e.getSeverity() == XmlSeverity.ERROR;
}
@Override
public PmdXmlReporter newScope() {
return new PmdXmlReporterImpl(ooxml, positioner) {
@Override
protected void consumeEx(XmlException e) {
super.consumeEx(e);
PmdXmlReporterImpl.this.consumeEx(e);
}
};
}
@Override
public boolean hasError() {
return hasError;
}
@Override
public void close() {
}
}
private DocumentBuilder newDocumentBuilder() {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
dbf.setSchema(version.getSchema());
dbf.setNamespaceAware(true);
return dbf.newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
}
}
static class Reporter {
private final XmlPosition position;
private final XmlPositioner positioner;
private final OoxmlFacade ooxml;
private final Consumer<XmlException> handler;
private Reporter(XmlPosition position, XmlPositioner positioner, OoxmlFacade ooxml, Consumer<XmlException> handler) {
this.position = position;
this.positioner = positioner;
this.ooxml = ooxml;
this.handler = handler;
}
public void warn(String messageFormat, Object... args) {
reportImpl(XmlSeverity.WARNING, MessageFormat.format(messageFormat, args));
}
public void error(String messageFormat, Object... args) {
reportImpl(XmlSeverity.ERROR, MessageFormat.format(messageFormat, args));
}
private void reportImpl(XmlSeverity severity, String formattedMessage) {
NiceXmlMessageSpec spec =
new NiceXmlMessageSpec(position, formattedMessage)
.withSeverity(severity);
String fullMessage = ooxml.getFormatter().formatSpec(ooxml, spec, positioner);
XmlException ex = new XmlException(spec, fullMessage);
handler.accept(ex);
}
}
}

View File

@ -0,0 +1,53 @@
/*
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.test.schema;
import java.net.URL;
import java.util.Objects;
import javax.xml.XMLConstants;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.xml.sax.SAXException;
/**
* @author Clément Fournier
*/
public enum TestSchemaVersion {
V1("rule-tests_1_0_0.xsd", new BaseTestParserImpl.ParserV1());
private final Schema schema;
private String schemaLoc;
private BaseTestParserImpl parser;
TestSchemaVersion(String schemaLoc, BaseTestParserImpl parser) {
this.schemaLoc = schemaLoc;
this.parser = parser;
this.schema = parseSchema();
}
BaseTestParserImpl getParserImpl() {
return parser;
}
public Schema getSchema() {
return schema;
}
private Schema parseSchema() {
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
try {
return schemaFactory.newSchema(locateSchema());
} catch (SAXException e) {
throw new RuntimeException("Cannot parse schema " + this, e);
}
}
private URL locateSchema() {
URL resource = TestSchemaVersion.class.getResource(schemaLoc);
return Objects.requireNonNull(resource, "Cannot find schema location " + schemaLoc);
}
}

View File

@ -0,0 +1,5 @@
/**
* Contains a parser for the XML test format.
*/
package net.sourceforge.pmd.test.schema;

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<schema
xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://pmd.sourceforge.net/rule-tests"
xmlns:tns="http://pmd.sourceforge.net/rule-tests"
elementFormDefault="qualified">
<element name="test-data">
<complexType>
<group ref="tns:testCodeOrCodeFragment" minOccurs="1" maxOccurs="unbounded"/>
</complexType>
</element>
<group name="testCodeOrCodeFragment">
<sequence>
<element name="test-code" minOccurs="0" maxOccurs="unbounded" type="tns:testCodeType"/>
<element name="code-fragment" minOccurs="0" maxOccurs="unbounded" type="tns:codeFragmentType"/>
</sequence>
</group>
<complexType name="testCodeType">
<sequence>
<element name="description" type="string"/>
<element name="rule-property" minOccurs="0" maxOccurs="unbounded">
<complexType>
<simpleContent>
<extension base="string">
<attribute name="name" type="string" use="required"/>
</extension>
</simpleContent>
</complexType>
</element>
<element name="expected-problems" type="integer"/>
<element name="expected-linenumbers" type="string" minOccurs="0"/>
<element name="expected-messages" minOccurs="0">
<complexType>
<sequence>
<element name="message" type="string" maxOccurs="unbounded"/>
</sequence>
</complexType>
</element>
<choice>
<element name="code"/>
<element name="code-ref">
<complexType>
<attribute name="id" type="IDREF" use="required"/>
</complexType>
</element>
</choice>
<element name="source-type" minOccurs="0" default="java" type="string"/>
</sequence>
<attribute name="reinitializeRule" type="boolean" default="true"/>
<attribute name="regressionTest" type="boolean" default="true"/>
<attribute name="useAuxClasspath" type="boolean" default="true"/>
</complexType>
<complexType name="codeFragmentType">
<simpleContent>
<extension base="string">
<attribute name="id" type="ID" use="required"/>
</extension>
</simpleContent>
</complexType>
</schema>

View File

@ -12,6 +12,10 @@
<relativePath>../</relativePath>
</parent>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<!--
hamcrest and junit are scope compile,

View File

@ -1104,5 +1104,6 @@
<module>pmd-scala-modules/pmd-scala_2.13</module>
<module>pmd-scala-modules/pmd-scala_2.12</module>
<module>pmd-visualforce</module>
<module>pmd-test-schema</module>
</modules>
</project>