From 8a048fd18d96adcf5586e9d3d05238cf29a7a48a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Fournier?= Date: Fri, 15 Sep 2017 23:02:39 +0200 Subject: [PATCH] Rule factory and builder --- .../sourceforge/pmd/ruleset/RuleBuilder.java | 229 +++++++++++++ .../sourceforge/pmd/ruleset/RuleFactory.java | 317 ++++++++++++++++++ 2 files changed, 546 insertions(+) create mode 100644 pmd-core/src/main/java/net/sourceforge/pmd/ruleset/RuleBuilder.java create mode 100644 pmd-core/src/main/java/net/sourceforge/pmd/ruleset/RuleFactory.java diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/ruleset/RuleBuilder.java b/pmd-core/src/main/java/net/sourceforge/pmd/ruleset/RuleBuilder.java new file mode 100644 index 0000000000..7398b0663a --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/ruleset/RuleBuilder.java @@ -0,0 +1,229 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.ruleset; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import net.sourceforge.pmd.PropertyDescriptor; +import net.sourceforge.pmd.Rule; +import net.sourceforge.pmd.RulePriority; +import net.sourceforge.pmd.lang.Language; +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.LanguageVersion; + +/** + * Builds a rule, validating its parameters throughout. The builder can define property descriptors, but not override + * them. For that, use RuleFactory. + * + * @author Clément Fournier + * @since 6.0.0 + */ +class RuleBuilder { + + private List> definedProperties = new ArrayList<>(); + private String name; + private String clazz; + private Language language; + private LanguageVersion minimumVersion; + private LanguageVersion maximumVersion; + private String since; + private String message; + private String rulesetName; + private String externalInfoUrl; + private String description; + private List examples = new ArrayList<>(1); + private RulePriority priority; + private boolean isDeprecated; + private boolean isUsesDfa; + private boolean isUsesMetrics; + private boolean isUsesTyperesolution; + + + RuleBuilder(String name, String clazz, String language) { + this.name = name; + language(language); + className(clazz); + } + + + public void usesDFA(boolean usesDFA) { + isUsesDfa = usesDFA; + } + + + public void usesMetrics(boolean usesMetrics) { + isUsesMetrics = usesMetrics; + } + + + public void usesTyperesolution(boolean usesTyperesolution) { + isUsesTyperesolution = usesTyperesolution; + } + + + private RuleBuilder language(String languageName) { + if (StringUtils.isBlank(languageName)) { + throw new IllegalArgumentException("Blank language attribute"); + } + + Language lang = LanguageRegistry.findLanguageByTerseName(languageName); + if (lang == null) { + throw new IllegalArgumentException( + "Unknown Language '" + languageName + "' for rule" + name + ", supported Languages are " + + LanguageRegistry.commaSeparatedTerseNamesForLanguage(LanguageRegistry.findWithRuleSupport())); + } + language = lang; + return this; + } + + + private RuleBuilder className(String className) { + if (StringUtils.isBlank(className)) { + throw new IllegalArgumentException("The 'class' field of rule can't be null, nor empty."); + } + + this.clazz = className; + return this; + } + + + public RuleBuilder minimumLanguageVersion(String minimum) { + LanguageVersion minimumLanguageVersion = language.getVersion(minimum); + if (minimumLanguageVersion == null) { + throw new IllegalArgumentException("Unknown minimum Language Version '" + minimum + + "' for Language '" + language.getTerseName() + "' for rule" + + name + + "; supported Language Versions are: " + + LanguageRegistry.commaSeparatedTerseNamesForLanguageVersion(language.getVersions())); + } + + minimumVersion = minimumLanguageVersion; + checkLanguageVersionsAreOrdered(); + return this; + } + + + public RuleBuilder maximumLanguageVersion(String maximum) { + LanguageVersion maximumLanguageVersion = language.getVersion(maximum); + if (maximumLanguageVersion == null) { + throw new IllegalArgumentException("Unknown maximum Language Version '" + maximum + + "' for Language '" + language.getTerseName() + + "' for Rule " + name + + "; supported Language Versions are: " + + LanguageRegistry.commaSeparatedTerseNamesForLanguageVersion(language.getVersions())); + } + maximumVersion = maximumLanguageVersion; + checkLanguageVersionsAreOrdered(); + return this; + } + + + private void checkLanguageVersionsAreOrdered() { + if (minimumVersion != null && maximumVersion != null + && minimumVersion.compareTo(maximumVersion) > 0) { + throw new IllegalArgumentException( + "The minimum Language Version '" + minimumVersion.getTerseName() + + "' must be prior to the maximum Language Version '" + + maximumVersion.getTerseName() + "' for Rule '" + name + + "'; perhaps swap them around?"); + } + } + + + public RuleBuilder since(String sinceStr) { + if (StringUtils.isNotBlank(sinceStr)) { + since = sinceStr; + } + return this; + } + + + public void externalInfoUrl(String externalInfoUrl) { + this.externalInfoUrl = externalInfoUrl; + } + + + public void rulesetName(String rulesetName) { + this.rulesetName = rulesetName; + } + + + public void message(String message) { + this.message = message; + } + + + public void defineProperty(PropertyDescriptor descriptor) { + definedProperties.add(descriptor); + } + + + public void setDeprecated(boolean deprecated) { + isDeprecated = deprecated; + } + + + public void description(String description) { + this.description = description; + } + + + public void addExample(String example) { + examples.add(example); + } + + + public void priority(int priority) { + this.priority = RulePriority.valueOf(priority); + } + + + public Rule build() throws ClassNotFoundException, IllegalAccessException, InstantiationException { + + + Rule rule = (Rule) RuledefFile.class.getClassLoader().loadClass(clazz).newInstance(); + + rule.setName(name); + rule.setRuleClass(clazz); + rule.setLanguage(language); + rule.setMinimumLanguageVersion(minimumVersion); + rule.setMaximumLanguageVersion(maximumVersion); + rule.setSince(since); + rule.setMessage(message); + rule.setExternalInfoUrl(externalInfoUrl); + rule.setDeprecated(isDeprecated); + rule.setDescription(description); + rule.setPriority(priority); + + for (String example : examples) { + rule.addExample(example); + } + + + if (isUsesDfa) { + rule.setUsesDFA(); + } + + if (isUsesMetrics) { + rule.setUsesMetrics(); + } + + if (isUsesTyperesolution) { + rule.setUsesTypeResolution(); + } + + for (PropertyDescriptor descriptor : definedProperties) { + rule.definePropertyDescriptor(descriptor); + } + + + return rule; + } + + +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/ruleset/RuleFactory.java b/pmd-core/src/main/java/net/sourceforge/pmd/ruleset/RuleFactory.java new file mode 100644 index 0000000000..55bfeb3f48 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/ruleset/RuleFactory.java @@ -0,0 +1,317 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.ruleset; + +import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import net.sourceforge.pmd.AbstractPropertyDescriptorFactory; +import net.sourceforge.pmd.PropertyDescriptor; +import net.sourceforge.pmd.PropertyDescriptorFactory; +import net.sourceforge.pmd.PropertyDescriptorField; +import net.sourceforge.pmd.Rule; +import net.sourceforge.pmd.lang.rule.properties.PropertyDescriptorUtil; + +/** + * Builds rules from rule XML nodes. You cannot use a single one to concurrently build several rules. + * + * @author Clément Fournier + * @since 6.0.0 + */ +public class RuleFactory { + + public static final RuleFactory INSTANCE = new RuleFactory(); + + + private RuleFactory() { + + } + + + /** + * Parses a rule element and returns a new rule instance. + * + * + *

Notes: The ruleset name is not set here. Exceptions thrown from this class indicate invalid XML structure, + * with regards to the expected schema, while RuleBuilder validates the semantics. + * + * @param ruleElement The rule element to parse + * + * @return A new instance of the rule in this element + */ + public Rule buildRule(Element ruleElement) { + + checkRequiredAttributesArePresent(ruleElement); + + String name = ruleElement.getAttribute("name"); + + RuleBuilder builder = new RuleBuilder(name, + ruleElement.getAttribute("class"), + ruleElement.getAttribute("language")); + + + if (ruleElement.hasAttribute("minimumLanguageVersion")) { + builder.minimumLanguageVersion(ruleElement.getAttribute("minimumLanguageVersion")); + } + + if (ruleElement.hasAttribute("maximumLanguageVersion")) { + builder.maximumLanguageVersion(ruleElement.getAttribute("maximumLanguageVersion")); + } + + if (ruleElement.hasAttribute("since")) { + builder.since(ruleElement.getAttribute("since")); + } + + builder.since(ruleElement.getAttribute("since")); + builder.message(ruleElement.getAttribute("message")); + builder.externalInfoUrl(ruleElement.getAttribute("externalInfoUrl")); + builder.setDeprecated(hasAttributeSetTrue(ruleElement, "deprecated")); + builder.usesDFA(hasAttributeSetTrue(ruleElement, "dfa")); + builder.usesTyperesolution(hasAttributeSetTrue(ruleElement, "typeResolution")); + builder.usesMetrics(hasAttributeSetTrue(ruleElement, "metrics")); + + + Element propertiesElement = null; + + final NodeList nodeList = ruleElement.getChildNodes(); + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + if (node.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + String nodeName = node.getNodeName(); + switch (nodeName) { + case "description": + builder.description(parseTextNode(node)); + break; + case "example": + builder.addExample(parseTextNode(node)); + break; + case "priority": + builder.priority(Integer.parseInt(parseTextNode(node).trim())); + break; + case "properties": + parsePropertiesForDefinitions(builder, node); + propertiesElement = (Element) node; + break; + default: + throw new IllegalArgumentException("Unexpected element <" + nodeName + + "> encountered as child of element for Rule " + + name); + } + } + + Rule rule; + try { + rule = builder.build(); + } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { + e.printStackTrace(); + return null; + } + + + if (propertiesElement != null) { + overrideProperties(rule, propertiesElement); + } + + return rule; + } + + + private void checkRequiredAttributesArePresent(Element ruleElement) { + final List required = Arrays.asList("name", "class", "language"); + + for (String att : required) { + if (!ruleElement.hasAttribute(att)) { + throw new IllegalArgumentException("Missing '" + att + "' attribute"); + } + } + } + + + /** + * Parses a properties element looking only for property value overrides. + * + * @param propertiesNode Node to parse + * + * @return The map of overridden properties names to their value + */ + private Map parsePropertiesForOverrides(Element propertiesNode) { + Map overridenProperties = new HashMap<>(); + + for (int i = 0; i < propertiesNode.getChildNodes().getLength(); i++) { + Node node = propertiesNode.getChildNodes().item(i); + if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals("property") + && !isPropertyDefinition((Element) node)) { + Entry overridden = parsePropertyOverride((Element) node); + overridenProperties.put(overridden.getKey(), overridden.getValue()); + } + } + + return overridenProperties; + } + + + /** + * Parses the properties node and adds property definitions to the builder. Doesn't care for value overriding, that + * will be handled after the rule instantiation. + * + * @param builder Rule builder + * @param propertiesNode Node to parse + */ + private void parsePropertiesForDefinitions(RuleBuilder builder, Node propertiesNode) { + + for (int i = 0; i < propertiesNode.getChildNodes().getLength(); i++) { + Node node = propertiesNode.getChildNodes().item(i); + if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals("property") + && isPropertyDefinition((Element) node)) { + PropertyDescriptor descriptor = parsePropertyDefinition((Element) node); + builder.defineProperty(descriptor); + } + } + } + + + private Entry parsePropertyOverride(Element propertyElement) { + String name = propertyElement.getAttribute(PropertyDescriptorField.NAME.attributeName()); + return new SimpleEntry<>(name, valueFrom(propertyElement)); + } + + + /** + * Overrides the rule's properties with the values defined in the element. + * + * @param rule The rule + * @param propertiesElt The {@literal } element + */ + private void overrideProperties(Rule rule, Element propertiesElt) { + Map overridden = parsePropertiesForOverrides(propertiesElt); + + for (Entry e : overridden.entrySet()) { + PropertyDescriptor descriptor = rule.getPropertyDescriptor(e.getKey()); + if (descriptor == null) { + throw new IllegalArgumentException( + "Cannot set non-existent property '" + e.getKey() + "' on Rule " + rule.getName()); + } + + setRulePropertyCapture(rule, descriptor, e.getValue()); + } + } + + + private void setRulePropertyCapture(Rule rule, PropertyDescriptor descriptor, String value) { + rule.setProperty(descriptor, descriptor.valueFrom(value)); + } + + + private static boolean isPropertyDefinition(Element node) { + return StringUtils.isNotBlank(node.getAttribute(PropertyDescriptorField.TYPE.attributeName())); + } + + + /** + * Parses a property definition node and returns the defined property descriptor. + * + * @param propertyElement Property node to parse + * + * @return The property descriptor + */ + private static PropertyDescriptor parsePropertyDefinition(Element propertyElement) { + + String typeId = propertyElement.getAttribute(PropertyDescriptorField.TYPE.attributeName()); + String strValue = valueFrom(propertyElement); + + PropertyDescriptorFactory pdFactory = PropertyDescriptorUtil.factoryFor(typeId); + if (pdFactory == null) { + throw new RuntimeException("No property descriptor factory for type: " + typeId); + } + + Set valueKeys = pdFactory.expectableFields(); + Map values = new HashMap<>(valueKeys.size()); + + // populate a map of values for an individual descriptor + for (PropertyDescriptorField field : valueKeys) { + String valueStr = propertyElement.getAttribute(field.attributeName()); + if (valueStr != null) { + values.put(field, valueStr); + } + } + + if (StringUtils.isBlank(values.get(PropertyDescriptorField.DEFAULT_VALUE))) { + NodeList children = propertyElement.getElementsByTagName(PropertyDescriptorField.DEFAULT_VALUE.attributeName()); + if (children.getLength() == 1) { + values.put(PropertyDescriptorField.DEFAULT_VALUE, children.item(0).getTextContent()); + } else { + throw new RuntimeException("No value defined!"); + } + } + + // casting is not pretty but prevents the interface from having this method + return (PropertyDescriptor) ((AbstractPropertyDescriptorFactory) pdFactory).createExternalWith(values); + } + + + /** Gets the string value from a property node. */ + private static String valueFrom(Element propertyNode) { + + String strValue = propertyNode.getAttribute(PropertyDescriptorField.DEFAULT_VALUE.attributeName()); + + if (StringUtils.isNotBlank(strValue)) { + return strValue; + } + + final NodeList nodeList = propertyNode.getChildNodes(); + + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals("value")) { + return parseTextNode(node); + } + } + return null; + } + + + private static boolean hasAttributeSetTrue(Element element, String attributeId) { + return element.hasAttribute(attributeId) && "true".equalsIgnoreCase(element.getAttribute(attributeId)); + } + + + /** + * Parse a String from a textually type node. + * + * @param node The node. + * + * @return The String. + */ + private static String parseTextNode(Node node) { + + final int nodeCount = node.getChildNodes().getLength(); + if (nodeCount == 0) { + return ""; + } + + StringBuilder buffer = new StringBuilder(); + + for (int i = 0; i < nodeCount; i++) { + Node childNode = node.getChildNodes().item(i); + if (childNode.getNodeType() == Node.CDATA_SECTION_NODE || childNode.getNodeType() == Node.TEXT_NODE) { + buffer.append(childNode.getNodeValue()); + } + } + return buffer.toString(); + } + +}