From ba602d19784653e4d849998013ff7c2b2d087928 Mon Sep 17 00:00:00 2001 From: Andreas Dangel Date: Thu, 11 Jan 2024 12:05:10 +0100 Subject: [PATCH] [core] Remove ruleset compatibility filter (RuleSetFactoryCompatibility) and CLI option `--no-ruleset-compatibility` Fixes #4314 Closes #4378 --- docs/pages/pmd/userdocs/migrating_to_pmd7.md | 1 + docs/pages/release_notes.md | 13 + .../java/net/sourceforge/pmd/ant/PMDTask.java | 9 - .../pmd/ant/internal/PMDTaskImpl.java | 1 - .../pmd/lang/apex/DefaultRulesetTest.java | 2 +- .../pmd/cli/commands/internal/PmdCommand.java | 9 - .../net/sourceforge/pmd/RuleSetFactory.java | 757 ++++++++++++++++++ .../pmd/RuleSetFactoryCompatibility.java | 3 + .../net/sourceforge/pmd/RuleSetLoader.java | 328 ++++++++ .../net/sourceforge/pmd/PMDConfiguration.java | 26 - .../net/sourceforge/pmd/RuleSetFactory.java | 6 - .../net/sourceforge/pmd/RuleSetLoader.java | 25 +- .../main/resources/rulesets/releases/34.xml | 2 + .../main/resources/rulesets/releases/35.xml | 2 + .../pmd/RuleSetFactoryCompatibilityTest.java | 103 --- .../sourceforge/pmd/RuleSetFactoryTest.java | 4 +- .../pmd/RulesetFactoryTestBase.java | 2 +- .../pmd/docs/RuleSetResolverTest.java | 7 +- .../pmd/lang/java/QuickstartRulesetTest.java | 2 +- 19 files changed, 1118 insertions(+), 184 deletions(-) create mode 100644 pmd-compat6/src/main/java/net/sourceforge/pmd/RuleSetFactory.java rename {pmd-core => pmd-compat6}/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java (98%) create mode 100644 pmd-compat6/src/main/java/net/sourceforge/pmd/RuleSetLoader.java delete mode 100644 pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryCompatibilityTest.java diff --git a/docs/pages/pmd/userdocs/migrating_to_pmd7.md b/docs/pages/pmd/userdocs/migrating_to_pmd7.md index 64b2a4f93d..098e7d32d6 100644 --- a/docs/pages/pmd/userdocs/migrating_to_pmd7.md +++ b/docs/pages/pmd/userdocs/migrating_to_pmd7.md @@ -174,6 +174,7 @@ Most notable changes: an error message such as `[main] ERROR net.sourceforge.pmd.cli.commands.internal.PmdCommand - No such file false`. * PMD tries to display a progress bar. If you don't want this (e.g. on a CI build server), you can disable this with `--no-progress`. + * `--no-ruleset-compatibility` has been removed ### Custom distribution packages diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index 9082e46f31..b78a6ab601 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -105,6 +105,8 @@ in the Migration Guide. * [#4723](https://github.com/pmd/pmd/issues/4723): \[cli] Launch fails for "bash pmd" * core * [#1027](https://github.com/pmd/pmd/issues/1027): \[core] Apply the new PropertyDescriptor<Pattern> type where applicable + * [#4314](https://github.com/pmd/pmd/issues/4314): \[core] Remove ruleset compatibility filter (RuleSetFactoryCompatibility) and CLI option `--no-ruleset-compatibility` + * [#4378](https://github.com/pmd/pmd/issues/4378): \[core] Ruleset loading processes commented rules * [#4674](https://github.com/pmd/pmd/issues/4674): \[core] WARNING: Illegal reflective access by org.codehaus.groovy.reflection.CachedClass * [#4694](https://github.com/pmd/pmd/pull/4694): \[core] Fix line/col numbers in TokenMgrError * [#4717](https://github.com/pmd/pmd/issues/4717): \[core] XSLTRenderer doesn't close report file @@ -159,6 +161,15 @@ The following previously deprecated classes have been removed: If the current version is needed, then `Node.getTextDocument().getLanguageVersion()` can be used. This is the version that has been selected via CLI `--use-version` parameter. +**Removed functionality** + +* The CLI parameter `--no-ruleset-compatibility` has been removed. It was only used to allow loading + some rulesets originally written for PMD 5 also in PMD 6 without fixing the rulesets. +* The class {% jdoc_old core::RuleSetFactoryCompatibility %} has been removed without replacement. + The different ways to enable/disable this filter in {% jdoc core::PMDConfiguration %} + (Property "RuleSetFactoryCompatibilityEnabled") and + {% jdoc ant::ant.PMDTask %} (Property "noRuleSetCompatibility") have been removed as well. + #### External Contributions * [#4640](https://github.com/pmd/pmd/pull/4640): \[cli] Launch script fails if run via "bash pmd" - [Shai Bennathan](https://github.com/shai-bennathan) (@shai-bennathan) * [#4673](https://github.com/pmd/pmd/pull/4673): \[javascript] CPD: Added support for decorator notation - [Wener](https://github.com/wener-tiobe) (@wener-tiobe) @@ -566,10 +577,12 @@ See also [Detailed Release Notes for PMD 7]({{ baseurl }}pmd_release_notes_pmd7. * [#4204](https://github.com/pmd/pmd/issues/4204): \[core] Provide a CpdAnalysis class as a programmatic entry point into CPD * [#4301](https://github.com/pmd/pmd/issues/4301): \[core] Remove deprecated property concrete classes * [#4302](https://github.com/pmd/pmd/issues/4302): \[core] Migrate Property Framework API to Java 8 + * [#4314](https://github.com/pmd/pmd/issues/4314): \[core] Remove ruleset compatibility filter (RuleSetFactoryCompatibility) and CLI option `--no-ruleset-compatibility` * [#4323](https://github.com/pmd/pmd/issues/4323): \[core] Refactor CPD integration * [#4353](https://github.com/pmd/pmd/pull/4353): \[core] Micro optimizations for Node API * [#4365](https://github.com/pmd/pmd/pull/4365): \[core] Improve benchmarking * [#4397](https://github.com/pmd/pmd/pull/4397): \[core] Refactor CPD + * [#4378](https://github.com/pmd/pmd/issues/4378): \[core] Ruleset loading processes commented rules * [#4420](https://github.com/pmd/pmd/pull/4420): \[core] Remove PMD.EOL * [#4425](https://github.com/pmd/pmd/pull/4425): \[core] Replace TextFile::pathId * [#4454](https://github.com/pmd/pmd/issues/4454): \[core] "Unknown option: '-min'" but is referenced in documentation diff --git a/pmd-ant/src/main/java/net/sourceforge/pmd/ant/PMDTask.java b/pmd-ant/src/main/java/net/sourceforge/pmd/ant/PMDTask.java index db10833ff7..eb9f7604b6 100644 --- a/pmd-ant/src/main/java/net/sourceforge/pmd/ant/PMDTask.java +++ b/pmd-ant/src/main/java/net/sourceforge/pmd/ant/PMDTask.java @@ -61,7 +61,6 @@ public class PMDTask extends Task { private final List relativizePathsWith = new ArrayList<>(); private String suppressMarker; private String rulesetFiles; - private boolean noRuleSetCompatibility; private String encoding; private int threads = 1; // same default as in PMDParameters (CLI) private int minimumPriority = RulePriority.LOW.getPriority(); // inclusive @@ -267,14 +266,6 @@ public class PMDTask extends Task { return nestedRules; } - public boolean isNoRuleSetCompatibility() { - return noRuleSetCompatibility; - } - - public void setNoRuleSetCompatibility(boolean noRuleSetCompatibility) { - this.noRuleSetCompatibility = noRuleSetCompatibility; - } - public String getCacheLocation() { return cacheLocation; } diff --git a/pmd-ant/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java b/pmd-ant/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java index 3bd399fc64..2633353fcc 100644 --- a/pmd-ant/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java +++ b/pmd-ant/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java @@ -68,7 +68,6 @@ public class PMDTaskImpl { configuration.setRuleSets(Arrays.asList(task.getRulesetFiles().split(","))); } - configuration.setRuleSetFactoryCompatibilityEnabled(!task.isNoRuleSetCompatibility()); if (task.getEncoding() != null) { configuration.setSourceEncoding(Charset.forName(task.getEncoding())); } diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/DefaultRulesetTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/DefaultRulesetTest.java index 3ca9685389..51379e4d2a 100644 --- a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/DefaultRulesetTest.java +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/DefaultRulesetTest.java @@ -39,6 +39,6 @@ class DefaultRulesetTest { } private RuleSetLoader rulesetLoader() { - return new RuleSetLoader().enableCompatibility(false); + return new RuleSetLoader(); } } diff --git a/pmd-cli/src/main/java/net/sourceforge/pmd/cli/commands/internal/PmdCommand.java b/pmd-cli/src/main/java/net/sourceforge/pmd/cli/commands/internal/PmdCommand.java index 29758b74be..bdeac806a7 100644 --- a/pmd-cli/src/main/java/net/sourceforge/pmd/cli/commands/internal/PmdCommand.java +++ b/pmd-cli/src/main/java/net/sourceforge/pmd/cli/commands/internal/PmdCommand.java @@ -102,8 +102,6 @@ public class PmdCommand extends AbstractAnalysisPmdSubcommand private String auxClasspath; - private boolean noRuleSetCompatibility; - private Path cacheLocation; private boolean noCache; @@ -213,12 +211,6 @@ public class PmdCommand extends AbstractAnalysisPmdSubcommand this.auxClasspath = auxClasspath; } - @Option(names = "--no-ruleset-compatibility", - description = "Disable the ruleset compatibility filter. The filter is active by default and tries automatically 'fix' old ruleset files with old rule names") - public void setNoRuleSetCompatibility(final boolean noRuleSetCompatibility) { - this.noRuleSetCompatibility = noRuleSetCompatibility; - } - @Option(names = "--cache", description = "Specify the location of the cache file for incremental analysis. " + "This should be the full path to the file, including the desired file name (not just the parent directory). " @@ -274,7 +266,6 @@ public class PmdCommand extends AbstractAnalysisPmdSubcommand configuration.addRelativizeRoots(relativizeRootPaths); } configuration.setRuleSets(rulesets); - configuration.setRuleSetFactoryCompatibilityEnabled(!this.noRuleSetCompatibility); configuration.setShowSuppressedViolations(showSuppressed); configuration.setSuppressMarker(suppressMarker); configuration.setThreads(threads); diff --git a/pmd-compat6/src/main/java/net/sourceforge/pmd/RuleSetFactory.java b/pmd-compat6/src/main/java/net/sourceforge/pmd/RuleSetFactory.java new file mode 100644 index 0000000000..7cb00ae650 --- /dev/null +++ b/pmd-compat6/src/main/java/net/sourceforge/pmd/RuleSetFactory.java @@ -0,0 +1,757 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +// This class has been taken from 7.0.0-SNAPSHOT +// Before removing RuleSetFactoryCompatibility (#4314) + +package net.sourceforge.pmd; + +import static net.sourceforge.pmd.util.CollectionUtil.setOf; +import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.DESCRIPTION; +import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.EXCLUDE; +import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.EXCLUDE_PATTERN; +import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.INCLUDE_PATTERN; +import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.NAME; +import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.PRIORITY; +import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.REF; +import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.RULE; +import static net.sourceforge.pmd.util.internal.xml.SchemaConstants.RULESET; + +import java.io.IOException; +import java.io.InputStream; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; +import java.util.zip.Adler32; +import java.util.zip.CheckedInputStream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.commons.lang3.StringUtils; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.event.Level; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import net.sourceforge.pmd.RuleSet.RuleSetBuilder; +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.rule.RuleReference; +import net.sourceforge.pmd.rules.RuleFactory; +import net.sourceforge.pmd.util.ResourceLoader; +import net.sourceforge.pmd.util.StringUtil; +import net.sourceforge.pmd.util.internal.xml.PmdXmlReporter; +import net.sourceforge.pmd.util.internal.xml.XmlErrorMessages; +import net.sourceforge.pmd.util.internal.xml.XmlUtil; +import net.sourceforge.pmd.util.log.MessageReporter; + +import com.github.oowekyala.ooxml.DomUtils; +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.XmlMessageHandler; +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; + +/** + * RuleSetFactory is responsible for creating RuleSet instances from XML + * content. See {@link RuleSetLoader} for configuration options and + * their defaults. + */ +final class RuleSetFactory { + + private static final Logger LOG = LoggerFactory.getLogger(RuleSetFactory.class); + + private final ResourceLoader resourceLoader; + private final LanguageRegistry languageRegistry; + private final RulePriority minimumPriority; + private final boolean warnDeprecated; + private final RuleSetFactoryCompatibility compatibilityFilter; + private final MessageReporter reporter; + private final boolean includeDeprecatedRuleReferences; + + private final Map parsedRulesets = new HashMap<>(); + + RuleSetFactory(ResourceLoader resourceLoader, + LanguageRegistry languageRegistry, + RulePriority minimumPriority, + boolean warnDeprecated, + RuleSetFactoryCompatibility compatFilter, + boolean includeDeprecatedRuleReferences, + MessageReporter reporter) { + this.resourceLoader = resourceLoader; + this.languageRegistry = Objects.requireNonNull(languageRegistry); + this.minimumPriority = minimumPriority; + this.warnDeprecated = warnDeprecated; + this.includeDeprecatedRuleReferences = includeDeprecatedRuleReferences; + + this.compatibilityFilter = compatFilter; + this.reporter = reporter; + } + + + /** + * Create a RuleSet from a RuleSetReferenceId. Priority filtering is ignored + * when loading a single Rule. The currently configured ResourceLoader is used. + * + * @param ruleSetReferenceId The RuleSetReferenceId of the RuleSet to create. + * + * @return A new RuleSet. + */ + @NonNull RuleSet createRuleSet(RuleSetReferenceId ruleSetReferenceId) { + return createRuleSet(ruleSetReferenceId, includeDeprecatedRuleReferences); + } + + private @NonNull RuleSet createRuleSet(RuleSetReferenceId ruleSetReferenceId, boolean withDeprecatedRuleReferences) + throws RuleSetLoadException { + return readDocument(ruleSetReferenceId, withDeprecatedRuleReferences); + } + + /** + * Create a Rule from a RuleSet created from a file name resource. The + * currently configured ResourceLoader is used. + *

+ * Any Rules in the RuleSet other than the one being created, are _not_ + * created. Deprecated rules are _not_ ignored, so that they can be + * referenced. + * + * @param ruleSetReferenceId + * The RuleSetReferenceId of the RuleSet with the Rule to create. + * @param withDeprecatedRuleReferences + * Whether RuleReferences that are deprecated should be ignored + * or not + * @return A new Rule. + */ + private Rule createRule(RuleSetReferenceId ruleSetReferenceId, boolean withDeprecatedRuleReferences) { + RuleSetReferenceId parentRuleset = ruleSetReferenceId.getParentRulesetIfThisIsARule(); + if (parentRuleset == null) { + throw new IllegalArgumentException( + "Cannot parse a single Rule from an all Rule RuleSet reference: <" + ruleSetReferenceId + ">."); + } + // can't use computeIfAbsent as creating a ruleset may add more entries to the map. + RuleSet ruleSet = parsedRulesets.get(parentRuleset); + if (ruleSet == null) { + ruleSet = createRuleSet(ruleSetReferenceId, withDeprecatedRuleReferences); + parsedRulesets.put(ruleSetReferenceId, ruleSet); + } + return ruleSet.getRuleByName(ruleSetReferenceId.getRuleName()); + } + + /** + * Parse a ruleset node to construct a RuleSet. + * + * @param ruleSetReferenceId The RuleSetReferenceId of the RuleSet being parsed. + * @param withDeprecatedRuleReferences whether rule references that are deprecated should be ignored + * or not + * + * @return The new RuleSet. + * + * @throws RuleSetLoadException If the ruleset cannot be parsed (eg IO exception, malformed XML, validation errors) + */ + private @NonNull RuleSet readDocument(RuleSetReferenceId ruleSetReferenceId, boolean withDeprecatedRuleReferences) { + + try (CheckedInputStream inputStream = new CheckedInputStream(ruleSetReferenceId.getInputStream(resourceLoader), new Adler32())) { + if (!ruleSetReferenceId.isExternal()) { + throw new IllegalArgumentException( + "Cannot parse a RuleSet from a non-external reference: <" + ruleSetReferenceId + ">."); + } + + XmlMessageHandler printer = getXmlMessagePrinter(); + DocumentBuilder builder = createDocumentBuilder(); + InputSource inputSource = new InputSource(inputStream); + inputSource.setSystemId(ruleSetReferenceId.getRuleSetFileName()); + + OoxmlFacade ooxml = new OoxmlFacade() + .withPrinter(printer) + .withAnsiColors(false); + PositionedXmlDoc parsed = ooxml.parse(builder, inputSource); + + @SuppressWarnings("PMD.CloseResource") + PmdXmlReporterImpl err = new PmdXmlReporterImpl(reporter, ooxml, parsed.getPositioner()); + try { + RuleSetBuilder ruleSetBuilder = new RuleSetBuilder(inputStream.getChecksum().getValue()).withFileName(ruleSetReferenceId.getRuleSetFileName()); + + RuleSet ruleSet = parseRulesetNode(ruleSetReferenceId, withDeprecatedRuleReferences, parsed, ruleSetBuilder, err); + if (err.errCount > 0) { + // note this makes us jump to the catch branch + // these might have been non-fatal errors + String message; + if (err.errCount == 1) { + message = "An XML validation error occurred"; + } else { + message = err.errCount + " XML validation errors occurred"; + } + throw new RuleSetLoadException(ruleSetReferenceId, message); + } + return ruleSet; + } catch (Exception | Error e) { + throw e; + } + } catch (ParserConfigurationException | IOException ex) { + throw new RuleSetLoadException(ruleSetReferenceId, ex); + } + } + + + private RuleSet parseRulesetNode(RuleSetReferenceId ruleSetReferenceId, + boolean withDeprecatedRuleReferences, + PositionedXmlDoc parsed, + RuleSetBuilder builder, + PmdXmlReporter err) { + Element ruleSetElement = parsed.getDocument().getDocumentElement(); + + if (ruleSetElement.hasAttribute("name")) { + builder.withName(ruleSetElement.getAttribute("name")); + } else { + err.at(ruleSetElement).warn("RuleSet name is missing. Future versions of PMD will require it."); + builder.withName("Missing RuleSet Name"); + } + + Set rulesetReferences = new HashSet<>(); + + for (Element node : DomUtils.children(ruleSetElement)) { + String text = XmlUtil.parseTextNode(node); + if (DESCRIPTION.matchesElt(node)) { + builder.withDescription(text); + } else if (INCLUDE_PATTERN.matchesElt(node)) { + final Pattern pattern = parseRegex(node, text, err); + if (pattern == null) { + continue; + } + builder.withFileInclusions(pattern); + } else if (EXCLUDE_PATTERN.matchesElt(node)) { + final Pattern pattern = parseRegex(node, text, err); + if (pattern == null) { + continue; + } + builder.withFileExclusions(pattern); + } else if (RULE.matchesElt(node)) { + try { + parseRuleNode(ruleSetReferenceId, builder, node, withDeprecatedRuleReferences, rulesetReferences, err); + } catch (XmlException ignored) { + // already reported (it's an XmlException), error count + // was incremented so parent method will throw RuleSetLoadException. + } + } else { + err.at(node).error(XmlErrorMessages.ERR__UNEXPECTED_ELEMENT_IN, + node.getTagName(), + RULESET); + } + } + + if (!builder.hasDescription()) { + err.at(ruleSetElement).warn("RuleSet description is missing. Future versions of PMD will require it."); + builder.withDescription("Missing description"); + } + + builder.filterRulesByPriority(minimumPriority); + + return builder.build(); + } + + private Pattern parseRegex(Element node, String text, PmdXmlReporter err) { + final Pattern pattern; + try { + pattern = Pattern.compile(text); + } catch (PatternSyntaxException pse) { + err.at(node).error(pse); + return null; + } + return pattern; + } + + + private DocumentBuilder createDocumentBuilder() throws ParserConfigurationException { + final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + + try { + /* + * parser hardening + * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Prevention_Cheat_Sheet#JAXP_DocumentBuilderFactory.2C_SAXParserFactory_and_DOM4J + */ + // This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented + // Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + + // If you can't completely disable DTDs, then at least do the following: + // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities + // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities + // JDK7+ - http://xml.org/sax/features/external-general-entities + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + + // Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities + // Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities + // JDK7+ - http://xml.org/sax/features/external-parameter-entities + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + + // Disable external DTDs as well + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + + // and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks" + dbf.setXIncludeAware(false); + dbf.setExpandEntityReferences(false); + } catch (final ParserConfigurationException e) { + // an unsupported feature... too bad, but won't fail execution due to this + LOG.warn("Ignored unsupported XML Parser Feature for parsing rulesets", e); + } + + return dbf.newDocumentBuilder(); + } + + /** + * Parse a rule node. + * + * @param ruleSetReferenceId The RuleSetReferenceId of the RuleSet being parsed. + * @param ruleSetBuilder The RuleSet being constructed. + * @param ruleNode Must be a rule element node. + * @param withDeprecatedRuleReferences whether rule references that are deprecated should be ignored + * or not + * @param rulesetReferences keeps track of already processed complete ruleset references in order to log + * a warning + */ + private void parseRuleNode(RuleSetReferenceId ruleSetReferenceId, + RuleSetBuilder ruleSetBuilder, + Element ruleNode, + boolean withDeprecatedRuleReferences, + Set rulesetReferences, + PmdXmlReporter err) { + if (REF.hasAttribute(ruleNode)) { + String ref = REF.getAttributeOrThrow(ruleNode, err); + RuleSetReferenceId refId = parseReferenceAndWarn(ref, REF.getAttributeNode(ruleNode), err); + if (refId != null) { + if (refId.isAllRules()) { + parseRuleSetReferenceNode(ruleSetBuilder, ruleNode, ref, refId, rulesetReferences, err); + } else { + parseRuleReferenceNode(ruleSetReferenceId, ruleSetBuilder, ruleNode, ref, refId, withDeprecatedRuleReferences, err); + } + return; + } + } + parseSingleRuleNode(ruleSetReferenceId, ruleSetBuilder, ruleNode, err); + } + + /** + * Parse a rule node as an RuleSetReference for all Rules. Every Rule from + * the referred to RuleSet will be added as a RuleReference except for those + * explicitly excluded, below the minimum priority threshold for this + * RuleSetFactory, or which are deprecated. + * + * @param ruleSetBuilder + * The RuleSet being constructed. + * @param ruleElement + * Must be a rule element node. + * @param ref + * The RuleSet reference. + * @param rulesetReferences keeps track of already processed complete ruleset references in order to log a warning + */ + private void parseRuleSetReferenceNode(RuleSetBuilder ruleSetBuilder, + Element ruleElement, + String ref, + RuleSetReferenceId ruleSetReferenceId, Set rulesetReferences, + PmdXmlReporter err) { + RulePriority priority = null; + Map excludedRulesCheck = new HashMap<>(); + for (Element child : XmlUtil.getElementChildrenList(ruleElement)) { + if (EXCLUDE.matchesElt(child)) { + String excludedRuleName; + try { + excludedRuleName = NAME.getAttributeOrThrow(child, err); + } catch (XmlException ignored) { + // has been reported + continue; + } + excludedRuleName = compatibilityFilter.applyExclude(ref, excludedRuleName, this.warnDeprecated); + if (excludedRuleName != null) { + excludedRulesCheck.put(excludedRuleName, child); + } + } else if (PRIORITY.matchesElt(child)) { + priority = RuleFactory.parsePriority(err, child); + } else { + XmlUtil.reportIgnoredUnexpectedElt(ruleElement, child, setOf(EXCLUDE, PRIORITY), err); + } + } + final RuleSetReference ruleSetReference = new RuleSetReference(ref, true, excludedRulesCheck.keySet()); + + // load the ruleset with minimum priority low, so that we get all rules, to be able to exclude any rule + // minimum priority will be applied again, before constructing the final ruleset + RuleSetFactory ruleSetFactory = toLoader().filterAbovePriority(RulePriority.LOW).warnDeprecated(false).toFactory(); + RuleSet otherRuleSet = ruleSetFactory.createRuleSet(ruleSetReferenceId); + List potentialRules = new ArrayList<>(); + int countDeprecated = 0; + for (Rule rule : otherRuleSet.getRules()) { + excludedRulesCheck.remove(rule.getName()); + if (!ruleSetReference.getExcludes().contains(rule.getName())) { + RuleReference ruleReference = new RuleReference(rule, ruleSetReference); + // override the priority + if (priority != null) { + ruleReference.setPriority(priority); + } + + if (rule.isDeprecated()) { + countDeprecated++; + } + potentialRules.add(ruleReference); + } + } + + boolean rulesetDeprecated = false; + if (!potentialRules.isEmpty() && potentialRules.size() == countDeprecated) { + // all rules in the ruleset have been deprecated - the ruleset itself is considered to be deprecated + rulesetDeprecated = true; + err.at(REF.getAttributeNode(ruleElement)) + .warn("The RuleSet {0} has been deprecated and will be removed in PMD {1}", + ref, PMDVersion.getNextMajorRelease()); + } + + for (RuleReference r : potentialRules) { + if (rulesetDeprecated || !r.getRule().isDeprecated()) { + // add the rule, if either the ruleset itself is deprecated (then we add all rules) + // or if the rule is not deprecated (in that case, the ruleset might contain deprecated as well + // as valid rules) + ruleSetBuilder.addRuleIfNotExists(r); + } + } + + if (!excludedRulesCheck.isEmpty()) { + excludedRulesCheck.forEach( + (name, elt) -> + err.at(elt).warn("Exclude pattern ''{0}'' did not match any rule in ruleset ''{1}''", name, ref)); + } + + if (rulesetReferences.contains(ref)) { + err.at(ruleElement).warn("The ruleset {0} is referenced multiple times in ruleset ''{1}''", ref, ruleSetBuilder.getName()); + } + rulesetReferences.add(ref); + } + + private RuleSetReferenceId parseReferenceAndWarn(String ref, + Node xmlPlace, + PmdXmlReporter err) { + ref = compatibilityFilter.applyRef(ref, this.warnDeprecated); + if (ref == null) { + err.at(xmlPlace).warn("Rule reference references a deleted rule, ignoring"); + return null; // deleted rule + } + // only emit a warning if we check for deprecated syntax + MessageReporter subReporter = warnDeprecated ? err.at(xmlPlace) : MessageReporter.quiet(); + + List references = RuleSetReferenceId.parse(ref, subReporter); + if (references.size() > 1 && warnDeprecated) { + err.at(xmlPlace).warn("Using a comma separated list as a ref attribute is deprecated. " + + "All references but the first are ignored."); + } else if (references.isEmpty()) { + err.at(xmlPlace).warn("Empty ref attribute"); + return null; + } + return references.get(0); + } + + /** + * Parse a rule node as a single Rule. The Rule has been fully defined + * within the context of the current RuleSet. + * + * @param ruleSetReferenceId The RuleSetReferenceId of the RuleSet being parsed. + * @param ruleSetBuilder The RuleSet being constructed. + * @param ruleNode Must be a rule element node. + * @param err Error reporter + */ + private void parseSingleRuleNode(RuleSetReferenceId ruleSetReferenceId, + RuleSetBuilder ruleSetBuilder, + Element ruleNode, + PmdXmlReporter err) { + + // Stop if we're looking for a particular Rule, and this element is not + // it. + if (StringUtils.isNotBlank(ruleSetReferenceId.getRuleName()) + && !isRuleName(ruleNode, ruleSetReferenceId.getRuleName())) { + return; + } + Rule rule = new RuleFactory(resourceLoader, languageRegistry).buildRule(ruleNode, err); + rule.setRuleSetName(ruleSetBuilder.getName()); + + if (warnDeprecated && StringUtils.isBlank(ruleNode.getAttribute("language"))) { + err.at(ruleNode).warn( + "Rule {0}/{1} does not mention attribute language='{2}'," + + " please mention it explicitly to be compatible with PMD 7", + ruleSetReferenceId.getRuleSetFileName(), rule.getName(), + rule.getLanguage().getTerseName()); + } + + ruleSetBuilder.addRule(rule); + } + + + /** + * Parse a rule node as a RuleReference. A RuleReference is a single Rule + * which comes from another RuleSet with some of it's attributes potentially + * overridden. + * + * @param ruleSetReferenceId The RuleSetReferenceId of the RuleSet being parsed. + * @param ruleSetBuilder The RuleSet being constructed. + * @param ruleNode Must be a rule element node. + * @param ref A reference to a Rule. + * @param withDeprecatedRuleReferences whether rule references that are deprecated should be ignored + * @param err Error reporter + */ + private void parseRuleReferenceNode(RuleSetReferenceId ruleSetReferenceId, + RuleSetBuilder ruleSetBuilder, + Element ruleNode, + String ref, + RuleSetReferenceId otherRuleSetReferenceId, + boolean withDeprecatedRuleReferences, + PmdXmlReporter err) { + + // Stop if we're looking for a particular Rule, and this element is not + // it. + if (StringUtils.isNotBlank(ruleSetReferenceId.getRuleName()) + && !isRuleName(ruleNode, ruleSetReferenceId.getRuleName())) { + return; + } + + // load the ruleset with minimum priority low, so that we get all rules, to be able to exclude any rule + // minimum priority will be applied again, before constructing the final ruleset + RuleSetFactory ruleSetFactory = toLoader().filterAbovePriority(RulePriority.LOW).warnDeprecated(false).toFactory(); + + boolean isSameRuleSet = false; + if (!otherRuleSetReferenceId.isExternal() + && containsRule(ruleSetReferenceId, otherRuleSetReferenceId.getRuleName())) { + otherRuleSetReferenceId = new RuleSetReferenceId(ref, ruleSetReferenceId, err.at(REF.getAttributeNode(ruleNode))); + isSameRuleSet = true; + } else if (otherRuleSetReferenceId.isExternal() + && otherRuleSetReferenceId.getRuleSetFileName().equals(ruleSetReferenceId.getRuleSetFileName())) { + otherRuleSetReferenceId = new RuleSetReferenceId(otherRuleSetReferenceId.getRuleName(), ruleSetReferenceId, err.at(REF.getAttributeNode(ruleNode))); + isSameRuleSet = true; + } + // do not ignore deprecated rule references + Rule referencedRule = ruleSetFactory.createRule(otherRuleSetReferenceId, true); + + if (referencedRule == null) { + throw err.at(ruleNode).error( + "Unable to find referenced rule {0}" + + "; perhaps the rule name is misspelled?", + otherRuleSetReferenceId.getRuleName()); + } + + if (warnDeprecated && referencedRule.isDeprecated()) { + if (referencedRule instanceof RuleReference) { + RuleReference ruleReference = (RuleReference) referencedRule; + err.at(ruleNode).warn( + "Use Rule name {0}/{1} instead of the deprecated Rule name {2}. PMD {3}" + + " will remove support for this deprecated Rule name usage.", + ruleReference.getRuleSetReference().getRuleSetFileName(), + ruleReference.getOriginalName(), otherRuleSetReferenceId, + PMDVersion.getNextMajorRelease()); + } else { + err.at(ruleNode).warn( + "Discontinue using Rule name {0} as it is scheduled for removal from PMD." + + " PMD {1} will remove support for this Rule.", + otherRuleSetReferenceId, PMDVersion.getNextMajorRelease()); + } + } + + RuleSetReference ruleSetReference = new RuleSetReference(otherRuleSetReferenceId.getRuleSetFileName(), false); + + RuleReference ruleReference; + try { + ruleReference = new RuleFactory(resourceLoader, languageRegistry).decorateRule(referencedRule, ruleSetReference, ruleNode, err); + } catch (XmlException e) { + throw err.at(ruleNode).error(e, "Error while parsing rule reference"); + } + + if (warnDeprecated && ruleReference.isDeprecated() && !isSameRuleSet) { + err.at(ruleNode).warn( + "Use Rule name {0}/{1} instead of the deprecated Rule name {2}/{3}. PMD {4}" + + " will remove support for this deprecated Rule name usage.", + ruleReference.getRuleSetReference().getRuleSetFileName(), + ruleReference.getOriginalName(), + ruleSetReferenceId.getRuleSetFileName(), + ruleReference.getName(), + PMDVersion.getNextMajorRelease()); + } + + if (withDeprecatedRuleReferences || !isSameRuleSet || !ruleReference.isDeprecated()) { + Rule existingRule = ruleSetBuilder.getExistingRule(ruleReference); + if (existingRule instanceof RuleReference) { + RuleReference existingRuleReference = (RuleReference) existingRule; + // the only valid use case is: the existing rule does not override anything yet + // which means, it is a plain reference. And the new reference overrides. + // for all other cases, we should log a warning + if (existingRuleReference.hasOverriddenAttributes() || !ruleReference.hasOverriddenAttributes()) { + err.at(ruleNode).warn( + "The rule {0} is referenced multiple times in ruleset ''{1}''. " + + "Only the last rule configuration is used.", + ruleReference.getName(), + ruleSetBuilder.getName()); + } + } + + ruleSetBuilder.addRuleReplaceIfExists(ruleReference); + } + } + + + /** + * Check whether the given ruleName is contained in the given ruleset. + * + * @param ruleSetReferenceId the ruleset to check + * @param ruleName the rule name to search for + * + * @return {@code true} if the ruleName exists + */ + private boolean containsRule(RuleSetReferenceId ruleSetReferenceId, String ruleName) { + // TODO: avoid reloading the ruleset once again + boolean found = false; + try (InputStream ruleSet = ruleSetReferenceId.getInputStream(resourceLoader)) { + DocumentBuilder builder = createDocumentBuilder(); + Document document = builder.parse(ruleSet); + Element ruleSetElement = document.getDocumentElement(); + + NodeList rules = ruleSetElement.getElementsByTagName("rule"); + for (int i = 0; i < rules.getLength(); i++) { + Element rule = (Element) rules.item(i); + if (rule.hasAttribute("name") && rule.getAttribute("name").equals(ruleName)) { + found = true; + break; + } + } + } catch (Exception e) { + throw new RuleSetLoadException(ruleSetReferenceId, e); + } + + return found; + } + + /** + * Determine if the specified rule element will represent a Rule with the + * given name. + * + * @param ruleElement The rule element. + * @param ruleName The Rule name. + * + * @return {@code true} if the Rule would have the given name, {@code false} otherwise. + */ + private boolean isRuleName(Element ruleElement, String ruleName) { + if (ruleElement.hasAttribute("name")) { + return ruleElement.getAttribute("name").equals(ruleName); + } else if (ruleElement.hasAttribute("ref")) { + RuleSetReferenceId ruleSetReferenceId = RuleSetReferenceId.parse(ruleElement.getAttribute("ref")).get(0); + return ruleSetReferenceId.getRuleName() != null && ruleSetReferenceId.getRuleName().equals(ruleName); + } else { + return false; + } + } + + + /** + * Create a new {@link RuleSetLoader} with the same config as this + * factory. This is a transitional API. + */ + public RuleSetLoader toLoader() { + return new RuleSetLoader().loadResourcesWith(resourceLoader) + .filterAbovePriority(minimumPriority) + .warnDeprecated(warnDeprecated) + .enableCompatibility(compatibilityFilter != null) + .includeDeprecatedRuleReferences(includeDeprecatedRuleReferences); + } + + private @NonNull XmlMessageHandler getXmlMessagePrinter() { + return entry -> { + Level level = entry.getSeverity() == XmlSeverity.WARNING ? Level.WARN : Level.ERROR; + String quotedText = StringUtil.quoteMessageFormat(entry.toString()); + reporter.logEx(level, quotedText, new Object[0], entry.getCause()); + }; + } + + private static final class PmdXmlReporterImpl + extends XmlMessageReporterBase + implements PmdXmlReporter { + + private final MessageReporter pmdReporter; + private int errCount; + + PmdXmlReporterImpl(MessageReporter pmdReporter, OoxmlFacade ooxml, XmlPositioner positioner) { + super(ooxml, positioner); + this.pmdReporter = pmdReporter; + } + + @Override + protected MessageReporter create2ndStage(XmlPosition position, XmlPositioner positioner) { + return new MessageReporter() { + @Override + public boolean isLoggable(Level level) { + return pmdReporter.isLoggable(level); + } + + + @Override + public void log(Level level, String message, Object... formatArgs) { + logEx(level, message, formatArgs, null); + } + + @Override + public void logEx(Level level, String message, Object[] formatArgs, @Nullable Throwable error) { + newException(level, error, message, formatArgs); + } + + @Override + public XmlException error(@Nullable Throwable cause, @Nullable String contextMessage, Object... formatArgs) { + return newException(Level.ERROR, cause, contextMessage, formatArgs); + } + + @Override + public XmlException newException(Level level, Throwable cause, String message, Object... formatArgs) { + XmlSeverity severity; + switch (level) { + case WARN: + severity = XmlSeverity.WARNING; + break; + case ERROR: + errCount++; + severity = XmlSeverity.ERROR; + break; + default: + throw new IllegalArgumentException("unexpected level " + level); + } + + if (message == null && formatArgs.length != 0) { + throw new IllegalArgumentException("Cannot pass format arguments for null message"); + } + + String actualMessage = message == null ? cause.getMessage() + : MessageFormat.format(message, formatArgs); + NiceXmlMessageSpec spec = + new NiceXmlMessageSpec(position, actualMessage) + .withSeverity(severity) + .withCause(cause); + String fullMessage = ooxml.getFormatter().formatSpec(ooxml, spec, positioner); + XmlException ex = new XmlException(spec, fullMessage); + ooxml.getPrinter().accept(ex); // spec of newException is also to log. + return ex; + } + + @Override + public int numErrors() { + return pmdReporter.numErrors(); + } + }; + } + } + +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java b/pmd-compat6/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java similarity index 98% rename from pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java rename to pmd-compat6/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java index 610676a92c..2e79697725 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java +++ b/pmd-compat6/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java @@ -2,6 +2,9 @@ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ +// This class has been taken from 7.0.0-SNAPSHOT +// Before removing RuleSetFactoryCompatibility (#4314) + package net.sourceforge.pmd; import java.text.MessageFormat; diff --git a/pmd-compat6/src/main/java/net/sourceforge/pmd/RuleSetLoader.java b/pmd-compat6/src/main/java/net/sourceforge/pmd/RuleSetLoader.java new file mode 100644 index 0000000000..4977eb2763 --- /dev/null +++ b/pmd-compat6/src/main/java/net/sourceforge/pmd/RuleSetLoader.java @@ -0,0 +1,328 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +// This class has been taken from 7.0.0-SNAPSHOT +// Before removing RuleSetFactoryCompatibility (#4314) + +package net.sourceforge.pmd; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Properties; + +import org.apache.commons.lang3.StringUtils; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.sourceforge.pmd.annotation.InternalApi; +import net.sourceforge.pmd.lang.Language; +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.util.CollectionUtil; +import net.sourceforge.pmd.util.ResourceLoader; +import net.sourceforge.pmd.util.log.MessageReporter; + +/** + * Configurable object to load rulesets from XML resources. + * This can be configured using a fluent API, see eg {@link #warnDeprecated(boolean)}. + * To create a new ruleset, use {@link #loadFromResource(String)} + * or some such overload. + */ +public final class RuleSetLoader { + private static final Logger LOG = LoggerFactory.getLogger(RuleSetLoader.class); + + private LanguageRegistry languageRegistry = LanguageRegistry.PMD; + private ResourceLoader resourceLoader = new ResourceLoader(RuleSetLoader.class.getClassLoader()); + private RulePriority minimumPriority = RulePriority.LOW; + private boolean warnDeprecated = true; + private @NonNull RuleSetFactoryCompatibility compatFilter = RuleSetFactoryCompatibility.DEFAULT; + private boolean includeDeprecatedRuleReferences = false; + private @NonNull MessageReporter reporter = MessageReporter.quiet(); + + /** + * Create a new RuleSetLoader with a default configuration. + * The defaults are described on each configuration method of this class. + */ + public RuleSetLoader() { // NOPMD UnnecessaryConstructor + // default + } + + RuleSetLoader withReporter(@NonNull MessageReporter reporter) { + this.reporter = Objects.requireNonNull(reporter); + return this; + } + + /** + * Specify that the given classloader should be used to resolve + * paths to external ruleset references. The default uses PMD's + * own classpath. + */ + public RuleSetLoader loadResourcesWith(ClassLoader classLoader) { + this.resourceLoader = new ResourceLoader(classLoader); + return this; + } + + // internal + RuleSetLoader loadResourcesWith(ResourceLoader loader) { + this.resourceLoader = loader; + return this; + } + + public RuleSetLoader withLanguages(LanguageRegistry languageRegistry) { + this.languageRegistry = languageRegistry; + return this; + } + + /** + * Filter loaded rules to only those that match or are above + * the given priority. The default is {@link RulePriority#LOW}, + * ie, no filtering occurs. + * + * @return This instance, modified + */ + public RuleSetLoader filterAbovePriority(RulePriority minimumPriority) { + this.minimumPriority = minimumPriority; + return this; + } + + /** + * Log a warning when referencing a deprecated rule. + * This is enabled by default. + * + * @return This instance, modified + */ + public RuleSetLoader warnDeprecated(boolean warn) { + this.warnDeprecated = warn; + return this; + } + + /** + * Enable translating old rule references to newer ones, if they have + * been moved or renamed. This is enabled by default, if disabled, + * unresolved references will not be translated and will produce an + * error. + * + * @return This instance, modified + */ + public RuleSetLoader enableCompatibility(boolean enable) { + return setCompatibility(enable ? RuleSetFactoryCompatibility.DEFAULT + : RuleSetFactoryCompatibility.EMPTY); + } + + // test only + RuleSetLoader setCompatibility(@NonNull RuleSetFactoryCompatibility filter) { + this.compatFilter = filter; + return this; + } + + /** + * Follow deprecated rule references. By default this is off, + * and those references will be ignored (with a warning depending + * on {@link #enableCompatibility(boolean)}). + * + * @return This instance, modified + */ + public RuleSetLoader includeDeprecatedRuleReferences(boolean enable) { + this.includeDeprecatedRuleReferences = enable; + return this; + } + + /** + * Create a new rule set factory, if you have to (that class is deprecated). + * That factory will use the configuration that was set using the setters of this. + * + * @deprecated {@link RuleSetFactory} is deprecated, replace its usages + * with usages of this class, or of static factory methods of {@link RuleSet} + */ + @Deprecated + public RuleSetFactory toFactory() { + return new RuleSetFactory( + this.resourceLoader, + this.languageRegistry, + this.minimumPriority, + this.warnDeprecated, + this.compatFilter, + this.includeDeprecatedRuleReferences, + this.reporter + ); + } + + private @Nullable MessageReporter filteredReporter() { + return warnDeprecated ? reporter : null; + } + + /** + * Parses and returns a ruleset from its location. The location may + * be a file system path, or a resource path (see {@link #loadResourcesWith(ClassLoader)}). + * + * @param rulesetPath A reference to a single ruleset + * + * @throws RuleSetLoadException If any error occurs (eg, invalid syntax, or resource not found) + */ + public RuleSet loadFromResource(String rulesetPath) { + return loadFromResource(new RuleSetReferenceId(rulesetPath, null, filteredReporter())); + } + + /** + * Parses and returns a ruleset from string content. + * + * @param filename The symbolic "file name", for error messages. + * @param rulesetXmlContent Xml file contents + * + * @throws RuleSetLoadException If any error occurs (eg, invalid syntax) + */ + public RuleSet loadFromString(String filename, final String rulesetXmlContent) { + return loadFromResource(new RuleSetReferenceId(filename, null, filteredReporter()) { + @Override + public InputStream getInputStream(ResourceLoader rl) { + return new ByteArrayInputStream(rulesetXmlContent.getBytes(StandardCharsets.UTF_8)); + } + }); + } + + /** + * Parses several resources into a list of rulesets. + * + * @param paths Paths + * + * @throws RuleSetLoadException If any error occurs (eg, invalid syntax, or resource not found), + * for any of the parameters + * @throws NullPointerException If the parameter, or any component is null + */ + public List loadFromResources(Collection paths) { + List ruleSets = new ArrayList<>(paths.size()); + for (String path : paths) { + ruleSets.add(loadFromResource(path)); + } + return ruleSets; + } + + /** + * Loads a list of rulesets, if any has an error, report it on the contextual + * error reporter instead of aborting, and continue loading the rest. + * + *

Internal API: might be published later, or maybe in PMD 7 this + * will be the default behaviour of every method of this class. + */ + @InternalApi + public List loadRuleSetsWithoutException(List rulesetPaths) { + List ruleSets = new ArrayList<>(rulesetPaths.size()); + boolean anyRules = false; + boolean error = false; + for (String path : rulesetPaths) { + try { + RuleSet ruleset = this.loadFromResource(path); + anyRules |= !ruleset.getRules().isEmpty(); + printRulesInDebug(path, ruleset); + ruleSets.add(ruleset); + } catch (RuleSetLoadException e) { + error = true; + reporter.error(e); + } + } + if (!anyRules && !error) { + reporter.warn("No rules found. Maybe you misspelled a rule name? ({0})", + StringUtils.join(rulesetPaths, ',')); + } + return ruleSets; + } + + void printRulesInDebug(String path, RuleSet ruleset) { + if (LOG.isDebugEnabled()) { + LOG.debug("Rules loaded from {}:", path); + for (Rule rule : ruleset.getRules()) { + LOG.debug("- {} ({})", rule.getName(), rule.getLanguage().getName()); + } + } + if (ruleset.getRules().isEmpty()) { + reporter.warn("No rules found in ruleset {0}", path); + } + + } + + /** + * Parses several resources into a list of rulesets. + * + * @param first First path + * @param rest Paths + * + * @throws RuleSetLoadException If any error occurs (eg, invalid syntax, or resource not found), + * for any of the parameters + * @throws NullPointerException If the parameter, or any component is null + */ + public List loadFromResources(String first, String... rest) { + return loadFromResources(CollectionUtil.listOf(first, rest)); + } + + // package private + RuleSet loadFromResource(RuleSetReferenceId ruleSetReferenceId) { + try { + return toFactory().createRuleSet(ruleSetReferenceId); + } catch (RuleSetLoadException e) { + throw e; + } catch (Exception e) { + throw new RuleSetLoadException(ruleSetReferenceId, e); + } + } + + + /** + * Configure a new ruleset factory builder according to the parameters + * of the given PMD configuration. + */ + public static RuleSetLoader fromPmdConfig(PMDConfiguration configuration) { + return new RuleSetLoader().filterAbovePriority(configuration.getMinimumPriority()) + .enableCompatibility(configuration.isRuleSetFactoryCompatibilityEnabled()) + .withLanguages(configuration.getLanguageRegistry()) + .withReporter(configuration.getReporter()); + } + + + /** + * Returns an Iterator of RuleSet objects loaded from descriptions from the + * "categories.properties" resource for each language. This + * uses the classpath of the resource loader ({@link #loadResourcesWith(ClassLoader)}). + * + * @return A list of all category rulesets + * + * @throws RuleSetLoadException If a standard ruleset cannot be loaded. + * This is a corner case, that probably should not be caught by clients. + * The standard rulesets are well-formed, at least in stock PMD distributions. + * + */ + public List getStandardRuleSets() { + String rulesetsProperties; + List ruleSetReferenceIds = new ArrayList<>(); + for (Language language : languageRegistry.getLanguages()) { + Properties props = new Properties(); + rulesetsProperties = "category/" + language.getTerseName() + "/categories.properties"; + try (InputStream inputStream = resourceLoader.loadClassPathResourceAsStreamOrThrow(rulesetsProperties)) { + props.load(inputStream); + String rulesetFilenames = props.getProperty("rulesets.filenames"); + // some languages might not have any rules and this property either doesn't exist or is empty + if (StringUtils.isNotBlank(rulesetFilenames)) { + ruleSetReferenceIds.addAll(Arrays.asList(rulesetFilenames.split(","))); + } + } catch (IOException e) { + throw new RuntimeException("Couldn't find " + rulesetsProperties + + "; please ensure that the directory is on the classpath. The current classpath is: " + + System.getProperty("java.class.path")); + } + } + + List ruleSets = new ArrayList<>(); + for (String id : ruleSetReferenceIds) { + ruleSets.add(loadFromResource(id)); // may throw + } + return ruleSets; + } +} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java b/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java index b262878dfe..d908167d69 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java @@ -44,9 +44,6 @@ import net.sourceforge.pmd.util.log.internal.SimpleMessageReporter; * These can be file paths or classpath resources. *

  • Use {@link #setMinimumPriority(RulePriority)} to control the minimum priority a * rule must have to be included. Defaults to the lowest priority, ie all rules are loaded.
  • - *
  • Use {@link #setRuleSetFactoryCompatibilityEnabled(boolean)} to disable the - * compatibility measures for removed and renamed rules in the rulesets that will - * be loaded. * * *

    Source files

    @@ -106,7 +103,6 @@ public class PMDConfiguration extends AbstractConfiguration { // Rule and source file options private List ruleSets = new ArrayList<>(); private RulePriority minimumPriority = RulePriority.LOW; - private boolean ruleSetFactoryCompatibilityEnabled = true; // Reporting options private String reportFormat; @@ -449,28 +445,6 @@ public class PMDConfiguration extends AbstractConfiguration { this.failOnViolation = failOnViolation; } - /** - * Checks if the rule set factory compatibility feature is enabled. - * - * @return true, if the rule set factory compatibility feature is enabled - * - * @see RuleSetLoader#enableCompatibility(boolean) - */ - public boolean isRuleSetFactoryCompatibilityEnabled() { - return ruleSetFactoryCompatibilityEnabled; - } - - /** - * Sets the rule set factory compatibility feature enabled/disabled. - * - * @param ruleSetFactoryCompatibilityEnabled {@code true} if the feature should be enabled - * - * @see RuleSetLoader#enableCompatibility(boolean) - */ - public void setRuleSetFactoryCompatibilityEnabled(boolean ruleSetFactoryCompatibilityEnabled) { - this.ruleSetFactoryCompatibilityEnabled = ruleSetFactoryCompatibilityEnabled; - } - /** * Retrieves the currently used analysis cache. Will never be null. * diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java index b78a5c6f5d..ab3bc2f5ac 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java @@ -80,7 +80,6 @@ final class RuleSetFactory { private final LanguageRegistry languageRegistry; private final RulePriority minimumPriority; private final boolean warnDeprecated; - private final RuleSetFactoryCompatibility compatibilityFilter; private final MessageReporter reporter; private final boolean includeDeprecatedRuleReferences; @@ -90,7 +89,6 @@ final class RuleSetFactory { LanguageRegistry languageRegistry, RulePriority minimumPriority, boolean warnDeprecated, - RuleSetFactoryCompatibility compatFilter, boolean includeDeprecatedRuleReferences, MessageReporter reporter) { this.resourceLoader = resourceLoader; @@ -99,7 +97,6 @@ final class RuleSetFactory { this.warnDeprecated = warnDeprecated; this.includeDeprecatedRuleReferences = includeDeprecatedRuleReferences; - this.compatibilityFilter = compatFilter; this.reporter = reporter; } @@ -374,7 +371,6 @@ final class RuleSetFactory { // has been reported continue; } - excludedRuleName = compatibilityFilter.applyExclude(ref, excludedRuleName, this.warnDeprecated); if (excludedRuleName != null) { excludedRulesCheck.put(excludedRuleName, child); } @@ -441,7 +437,6 @@ final class RuleSetFactory { private RuleSetReferenceId parseReferenceAndWarn(String ref, Node xmlPlace, PmdXmlReporter err) { - ref = compatibilityFilter.applyRef(ref, this.warnDeprecated); if (ref == null) { err.at(xmlPlace).warn("Rule reference references a deleted rule, ignoring"); return null; // deleted rule @@ -664,7 +659,6 @@ final class RuleSetFactory { return new RuleSetLoader().loadResourcesWith(resourceLoader) .filterAbovePriority(minimumPriority) .warnDeprecated(warnDeprecated) - .enableCompatibility(compatibilityFilter != null) .includeDeprecatedRuleReferences(includeDeprecatedRuleReferences); } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java index 15987ef0bc..acfc9116c4 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java @@ -41,7 +41,6 @@ public final class RuleSetLoader { private ResourceLoader resourceLoader = new ResourceLoader(RuleSetLoader.class.getClassLoader()); private RulePriority minimumPriority = RulePriority.LOW; private boolean warnDeprecated = true; - private @NonNull RuleSetFactoryCompatibility compatFilter = RuleSetFactoryCompatibility.DEFAULT; private boolean includeDeprecatedRuleReferences = false; private @NonNull MessageReporter reporter = MessageReporter.quiet(); @@ -102,29 +101,9 @@ public final class RuleSetLoader { return this; } - /** - * Enable translating old rule references to newer ones, if they have - * been moved or renamed. This is enabled by default, if disabled, - * unresolved references will not be translated and will produce an - * error. - * - * @return This instance, modified - */ - public RuleSetLoader enableCompatibility(boolean enable) { - return setCompatibility(enable ? RuleSetFactoryCompatibility.DEFAULT - : RuleSetFactoryCompatibility.EMPTY); - } - - // test only - RuleSetLoader setCompatibility(@NonNull RuleSetFactoryCompatibility filter) { - this.compatFilter = filter; - return this; - } - /** * Follow deprecated rule references. By default this is off, - * and those references will be ignored (with a warning depending - * on {@link #enableCompatibility(boolean)}). + * and those references will be ignored. * * @return This instance, modified */ @@ -147,7 +126,6 @@ public final class RuleSetLoader { this.languageRegistry, this.minimumPriority, this.warnDeprecated, - this.compatFilter, this.includeDeprecatedRuleReferences, this.reporter ); @@ -278,7 +256,6 @@ public final class RuleSetLoader { */ public static RuleSetLoader fromPmdConfig(PMDConfiguration configuration) { return new RuleSetLoader().filterAbovePriority(configuration.getMinimumPriority()) - .enableCompatibility(configuration.isRuleSetFactoryCompatibilityEnabled()) .withLanguages(configuration.getLanguageRegistry()) .withReporter(configuration.getReporter()); } diff --git a/pmd-core/src/main/resources/rulesets/releases/34.xml b/pmd-core/src/main/resources/rulesets/releases/34.xml index 2d95650dc1..67687bdc85 100644 --- a/pmd-core/src/main/resources/rulesets/releases/34.xml +++ b/pmd-core/src/main/resources/rulesets/releases/34.xml @@ -12,7 +12,9 @@ This ruleset contains links to rules that are new in PMD v3.4 + diff --git a/pmd-core/src/main/resources/rulesets/releases/35.xml b/pmd-core/src/main/resources/rulesets/releases/35.xml index 70cf98f608..b466b80c95 100644 --- a/pmd-core/src/main/resources/rulesets/releases/35.xml +++ b/pmd-core/src/main/resources/rulesets/releases/35.xml @@ -20,7 +20,9 @@ This ruleset contains links to rules that are new in PMD v3.5 + diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryCompatibilityTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryCompatibilityTest.java deleted file mode 100644 index f9e3c089bb..0000000000 --- a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryCompatibilityTest.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * BSD-style license; for more info see http://pmd.sourceforge.net/license.html - */ - -package net.sourceforge.pmd; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -import org.junit.jupiter.api.Test; - -class RuleSetFactoryCompatibilityTest { - - @Test - void testCorrectOldReference() throws Exception { - final String ruleset = "\n" + "\n" + "\n" - + " Test\n" + "\n" - + " \n" + "\n"; - - RuleSetFactoryCompatibility compat = new RuleSetFactoryCompatibility(); - compat.addFilterRuleMoved("dummy", "notexisting", "basic", "DummyBasicMockRule"); - - - RuleSetLoader rulesetLoader = new RuleSetLoader().setCompatibility(compat); - RuleSet createdRuleSet = rulesetLoader.loadFromString("dummy.xml", ruleset); - - assertNotNull(createdRuleSet.getRuleByName("DummyBasicMockRule")); - } - - @Test - void testCorrectMovedAndRename() { - - RuleSetFactoryCompatibility rsfc = new RuleSetFactoryCompatibility(); - rsfc.addFilterRuleMoved("dummy", "notexisting", "basic", "OldDummyBasicMockRule"); - rsfc.addFilterRuleRenamed("dummy", "basic", "OldDummyBasicMockRule", "NewNameForDummyBasicMockRule"); - - String out = rsfc.applyRef("rulesets/dummy/notexisting.xml/OldDummyBasicMockRule"); - - assertEquals("rulesets/dummy/basic.xml/NewNameForDummyBasicMockRule", out); - } - - @Test - void testExclusion() { - final String ruleset = "\n" + "\n" + "\n" - + " Test\n" + "\n" + " \n" - + " \n" + " \n" + "\n"; - - RuleSetFactoryCompatibility compat = new RuleSetFactoryCompatibility(); - compat.addFilterRuleRenamed("dummy", "basic", "OldNameOfSampleXPathRule", "SampleXPathRule"); - - RuleSetLoader rulesetLoader = new RuleSetLoader().setCompatibility(compat); - RuleSet createdRuleSet = rulesetLoader.loadFromString("dummy.xml", ruleset); - - assertNotNull(createdRuleSet.getRuleByName("DummyBasicMockRule")); - assertNull(createdRuleSet.getRuleByName("SampleXPathRule")); - } - - @Test - void testExclusionRenamedAndMoved() { - - RuleSetFactoryCompatibility rsfc = new RuleSetFactoryCompatibility(); - rsfc.addFilterRuleMovedAndRenamed("dummy", "oldbasic", "OldDummyBasicMockRule", "basic", "NewNameForDummyBasicMockRule"); - - String in = "rulesets/dummy/oldbasic.xml"; - String out = rsfc.applyRef(in); - - assertEquals(in, out); - } - - @Test - void testFilter() { - RuleSetFactoryCompatibility rsfc = new RuleSetFactoryCompatibility(); - rsfc.addFilterRuleMoved("dummy", "notexisting", "basic", "DummyBasicMockRule"); - rsfc.addFilterRuleRemoved("dummy", "basic", "DeletedRule"); - rsfc.addFilterRuleRenamed("dummy", "basic", "OldNameOfBasicMockRule", "NewNameOfBasicMockRule"); - - assertEquals("rulesets/dummy/basic.xml/DummyBasicMockRule", - rsfc.applyRef("rulesets/dummy/notexisting.xml/DummyBasicMockRule")); - - assertEquals("rulesets/dummy/basic.xml/NewNameOfBasicMockRule", - rsfc.applyRef("rulesets/dummy/basic.xml/OldNameOfBasicMockRule")); - - assertNull(rsfc.applyRef("rulesets/dummy/basic.xml/DeletedRule")); - } - - @Test - void testExclusionFilter() { - RuleSetFactoryCompatibility rsfc = new RuleSetFactoryCompatibility(); - rsfc.addFilterRuleRenamed("dummy", "basic", "AnotherOldNameOfBasicMockRule", "NewNameOfBasicMockRule"); - - String out = rsfc.applyExclude("rulesets/dummy/basic.xml", "AnotherOldNameOfBasicMockRule", false); - - assertEquals("NewNameOfBasicMockRule", out); - } - -} diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java index 541f88b477..72d2953824 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java @@ -525,7 +525,7 @@ class RuleSetFactoryTest extends RulesetFactoryTestBase { @Test void testReferencePriority() { - RuleSetLoader config = new RuleSetLoader().warnDeprecated(false).enableCompatibility(true); + RuleSetLoader config = new RuleSetLoader().warnDeprecated(false); RuleSetLoader rulesetLoader = config.filterAbovePriority(RulePriority.LOW); RuleSet ruleSet = rulesetLoader.loadFromString("", REF_INTERNAL_TO_INTERNAL_CHAIN); @@ -567,7 +567,7 @@ class RuleSetFactoryTest extends RulesetFactoryTestBase { @Test void testOverridePriorityLoadWithMinimum() { RuleSetLoader rulesetLoader = new RuleSetLoader().filterAbovePriority(RulePriority.MEDIUM_LOW) - .warnDeprecated(true).enableCompatibility(true); + .warnDeprecated(true); RuleSet ruleset = rulesetLoader.loadFromResource("net/sourceforge/pmd/rulesets/ruleset-minimum-priority.xml"); // only one rule should remain, since we filter out the other rule by minimum priority assertEquals(1, ruleset.getRules().size(), "Number of Rules"); diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/RulesetFactoryTestBase.java b/pmd-core/src/test/java/net/sourceforge/pmd/RulesetFactoryTestBase.java index f21796ee76..200ddbc6a7 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/RulesetFactoryTestBase.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/RulesetFactoryTestBase.java @@ -109,7 +109,7 @@ public class RulesetFactoryTestBase { try (PmdAnalysis pmd = PmdAnalysis.create(config)) { return pmd.newRuleSetLoader() .warnDeprecated(true) - .enableCompatibility(false).loadFromString("dummyRuleset.xml", ruleSetXml); + .loadFromString("dummyRuleset.xml", ruleSetXml); } } diff --git a/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleSetResolverTest.java b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleSetResolverTest.java index f5baf81b34..a42b4d1dd3 100644 --- a/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleSetResolverTest.java +++ b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleSetResolverTest.java @@ -13,9 +13,12 @@ import java.nio.file.Path; import java.util.List; import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.RuleSetLoader; import net.sourceforge.pmd.internal.util.IOUtil; +import net.sourceforge.pmd.util.log.internal.SimpleMessageReporter; class RuleSetResolverTest { @@ -33,7 +36,9 @@ class RuleSetResolverTest { assertFalse(additionalRulesets.isEmpty()); for (String filename : additionalRulesets) { - new RuleSetLoader().warnDeprecated(false).loadFromResource(filename); // will throw if invalid + PMDConfiguration config = new PMDConfiguration(); + config.setReporter(new SimpleMessageReporter(LoggerFactory.getLogger(RuleSetResolverTest.class))); + RuleSetLoader.fromPmdConfig(config).warnDeprecated(false).loadFromResource(filename); // will throw if invalid } } diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/QuickstartRulesetTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/QuickstartRulesetTest.java index 2b7ffdc9fd..67b2266e23 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/QuickstartRulesetTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/QuickstartRulesetTest.java @@ -20,7 +20,7 @@ class QuickstartRulesetTest { @Test void noDeprecations() throws Exception { - RuleSetLoader ruleSetLoader = new RuleSetLoader().enableCompatibility(false); + RuleSetLoader ruleSetLoader = new RuleSetLoader(); String errorOutput = SystemLambda.tapSystemErr(() -> { RuleSet quickstart = ruleSetLoader.loadFromResource(QUICKSTART_RULESET); assertFalse(quickstart.getRules().isEmpty());