diff --git a/.ci/inc/maven-dependencies.inc b/.ci/inc/maven-dependencies.inc index 0aaa020b81..12d1de380a 100644 --- a/.ci/inc/maven-dependencies.inc +++ b/.ci/inc/maven-dependencies.inc @@ -20,7 +20,18 @@ function maven_dependencies_resolve() { dokka_version=$(./mvnw -q -Dexec.executable="echo" -Dexec.args='${dokka.version}' \ --non-recursive org.codehaus.mojo:exec-maven-plugin:3.0.0:exec) - ./mvnw dependency:resolve + # build first the modules, that have dependencies between themselves + # first build pmd-lang-test, pmd-test and pmd-core - used by all modules + ./mvnw clean install -pl pmd-core,pmd-test,pmd-lang-test -DskipTests -Dpmd.skip=true \ + -Dcheckstyle.skip=true -Dmaven.javadoc.skip=true -Dmaven.source.skip=true + # then build dependencies for pmd-visualforce needs: pmd-apex->pmd-apex-jorje+pmd-test+pmd-core + ./mvnw clean install -pl pmd-core,pmd-test,pmd-lang-test,pmd-apex-jorje,pmd-apex -DskipTests -Dpmd.skip=true \ + -Dcheckstyle.skip=true -Dmaven.javadoc.skip=true -Dmaven.source.skip=true + + # the resolve most other projects. The excluded projects depend on other projects in the reactor, which is not + # completely built yet, so these are excluded. + ./mvnw dependency:resolve -pl '!pmd-dist,!pmd-doc,!pmd-scala' + ./mvnw dependency:get -DgroupId=org.jetbrains.dokka \ -DartifactId=dokka-maven-plugin \ -Dversion=${dokka_version} \ diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 56a5a13ccb..738f691819 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,8 @@ blank_issues_enabled: false contact_links: + - name: Question + url: https://github.com/pmd/pmd/discussions?discussions_q=category%3AQ%26A + about: Feel free to ask any question about PMD and its usage - name: PMD Designer Issues url: https://github.com/pmd/pmd-designer/issues about: Issues about the rule designer diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 5af12acfa1..0000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Question -about: Feel free to ask any question about PMD and its usage -title: '' -labels: 'a:question' -assignees: '' - ---- - - - -**Description:** - diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 2948cb6653..9e96a7f1ac 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -10,6 +10,10 @@ jobs: continue-on-error: false steps: - uses: actions/checkout@v2 + - name: Set up Ruby 2.7 + uses: actions/setup-ruby@v1 + with: + ruby-version: 2.7 - name: Check Environment run: .ci/check-environment.sh shell: bash diff --git a/README.md b/README.md index a819536f6d..038b529c54 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![PMD Logo](https://raw.githubusercontent.com/pmd/pmd/pmd/7.0.x/docs/images/logo/pmd-logo-300px.png) [![Join the chat at https://gitter.im/pmd/pmd](https://badges.gitter.im/pmd/pmd.svg)](https://gitter.im/pmd/pmd?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Build Status](https://github.com/pmd/pmd/workflows/.github/workflows/pushes.yml/badge.svg?branch=master)](https://github.com/pmd/pmd/actions) +[![Build Status](https://github.com/pmd/pmd/workflows/Pushes/badge.svg?branch=master)](https://github.com/pmd/pmd/actions) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/net.sourceforge.pmd/pmd/badge.svg)](https://maven-badges.herokuapp.com/maven-central/net.sourceforge.pmd/pmd) [![Reproducible Builds](https://img.shields.io/badge/Reproducible_Builds-ok-green?labelColor=blue)](https://github.com/jvm-repo-rebuild/reproducible-central#net.sourceforge.pmd:pmd) [![Coverage Status](https://coveralls.io/repos/github/pmd/pmd/badge.svg)](https://coveralls.io/github/pmd/pmd) diff --git a/docs/_config.yml b/docs/_config.yml index 252ef3bd60..90d8377a47 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -2,8 +2,8 @@ repository: pmd/pmd pmd: version: 7.0.0-SNAPSHOT - previous_version: 6.29.0 - date: ??-?????-2020 + previous_version: 6.30.0 + date: ??-?????-2021 release_type: major # release types: major, minor, bugfix diff --git a/docs/pages/next_major_development.md b/docs/pages/next_major_development.md index 31dd1316bd..f618b5117b 100644 --- a/docs/pages/next_major_development.md +++ b/docs/pages/next_major_development.md @@ -246,6 +246,47 @@ the breaking API changes will be performed in 7.0.0. an API is tagged as `@Deprecated` or not in the latest minor release. During the development of 7.0.0, we may decide to remove some APIs that were not tagged as deprecated, though we'll try to avoid it." %} +#### 6.30.0 + +##### Deprecated API + +###### Around RuleSet parsing + +* {% jdoc core::RuleSetFactory %} and {% jdoc core::RuleSetFactoryUtils %} have been deprecated in favor of {% jdoc core::RuleSetLoader %}. This is easier to configure, and more maintainable than the multiple overloads of `RuleSetFactoryUtils`. +* Some static creation methods have been added to {% jdoc core::RuleSet %} for simple cases, eg {% jdoc core::RuleSet#forSingleRule(core::Rule) %}. These replace some counterparts in {% jdoc core::RuleSetFactory %} +* Since {% jdoc core::RuleSets %} is also deprecated, many APIs that require a RuleSets instance now are deprecated, and have a counterpart that expects a `List`. +* {% jdoc core::RuleSetReferenceId %}, {% jdoc core::RuleSetReference %}, {% jdoc core::RuleSetFactoryCompatibility %} are deprecated. They are most likely not relevant outside of the implementation of pmd-core. + +###### Around the `PMD` class + +Many classes around PMD's entry point ({% jdoc core::PMD %}) have been deprecated as internal, including: +* The contents of the packages {% jdoc_package core::cli %}, {% jdoc_package core::processor %} +* {% jdoc core::SourceCodeProcessor %} +* The constructors of {% jdoc core::PMD %} (the class will be made a utility class) + +###### Miscellaneous + +* {% jdoc !!java::lang.java.ast.ASTPackageDeclaration#getPackageNameImage() %}, + {% jdoc !!java::lang.java.ast.ASTTypeParameter#getParameterName() %} + and the corresponding XPath attributes. In both cases they're replaced with a new method `getName`, + the attribute is `@Name`. +* {% jdoc !!java::lang.java.ast.ASTClassOrInterfaceBody#isAnonymousInnerClass() %}, + and {% jdoc !!java::lang.java.ast.ASTClassOrInterfaceBody#isEnumChild() %}, + refs [#905](https://github.com/pmd/pmd/issues/905) + +##### Internal API + +Those APIs are not intended to be used by clients, and will be hidden or removed with PMD 7.0.0. +You can identify them with the `@InternalApi` annotation. You'll also get a deprecation warning. + +* {% jdoc !!javascript::lang.ecmascript.Ecmascript3Handler %} +* {% jdoc !!javascript::lang.ecmascript.Ecmascript3Parser %} +* {% jdoc !!javascript::lang.ecmascript.ast.EcmascriptParser#parserOptions %} +* {% jdoc !!javascript::lang.ecmascript.ast.EcmascriptParser#getSuppressMap() %} +* {% jdoc !!core::lang.rule.ParametricRuleViolation %} +* {% jdoc !!core::lang.ParserOptions#suppressMarker %} +* {% jdoc !!modelica::lang.modelica.rule.ModelicaRuleViolationFactory %} + #### 6.29.0 No changes. @@ -1192,8 +1233,8 @@ large projects, with many duplications, it was causing `OutOfMemoryError`s (see will be removed with PMD 7.0.0. The rule is replaced by the more general {% rule "java/multithreading/UnsynchronizedStaticFormatter" %}. -* The two Java rules {% rule "java/bestpractices/PositionLiteralsFirstInComparisons" %} - and {% rule "java/bestpractices/PositionLiteralsFirstInCaseInsensitiveComparisons" %} (ruleset `java-bestpractices`) +* The two Java rules [`PositionLiteralsFirstInComparisons`](https://pmd.github.io/pmd-6.29.0/pmd_rules_java_bestpractices.html#positionliteralsfirstincomparisons) + and [`PositionLiteralsFirstInCaseInsensitiveComparisons`](https://pmd.github.io/pmd-6.29.0/pmd_rules_java_bestpractices.html#positionliteralsfirstincaseinsensitivecomparisons) (ruleset `java-bestpractices`) have been deprecated in favor of the new rule {% rule "java/bestpractices/LiteralsFirstInComparisons" %}. * The Java rule [`AvoidFinalLocalVariable`](https://pmd.github.io/pmd-6.16.0/pmd_rules_java_codestyle.html#avoidfinallocalvariable) (`java-codestyle`) has been deprecated diff --git a/docs/pages/pmd/devdocs/building.md b/docs/pages/pmd/devdocs/building.md index 0fcd7ce293..d9a7bcbc11 100644 --- a/docs/pages/pmd/devdocs/building.md +++ b/docs/pages/pmd/devdocs/building.md @@ -12,7 +12,7 @@ author: Tom Copeland, Xavier Le Vourch * JDK 11 or higher -{% include note.html content="While Java 11 is required for building, running PMD only requires Java 7 (or Java 8 for Apex and the Designer)." %} +{% include note.html content="While Java 11 is required for building, running PMD only requires Java 7 (or Java 8 for Apex, Scala, Visualforce, and the Designer)." %} You’ll need to either check out the source code or download the latest source release. Assuming you’ve got the latest source release, unzip it to a directory: diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index 4e21d55163..8abb75f161 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -19,78 +19,11 @@ This is a {{ site.pmd.release_type }} release. ### New and noteworthy -##### CPD - -* The C# module now supports the new option [`--ignore-literal-sequences`](https://pmd.github.io/latest/pmd_userdocs_cpd.html#-ignore-literal-sequences), which can be used to avoid detection of some uninteresting clones. Support for other languages may be added in the future. See [#2945](https://github.com/pmd/pmd/pull/2945) - -* The Scala module now supports [suppression](https://pmd.github.io/latest/pmd_userdocs_cpd.html#suppression) through `CPD-ON`/`CPD-OFF` comment pairs. See [#2929](https://github.com/pmd/pmd/pull/2929) - ### Fixed Issues -* core - * [#1939](https://github.com/pmd/pmd/issues/1939): \[core] XPath expressions return handling - * [#1961](https://github.com/pmd/pmd/issues/1961): \[core] Text renderer should include name of violated rule - * [#2874](https://github.com/pmd/pmd/pull/2874): \[core] Fix XMLRenderer with UTF-16 -* cs - * [#2938](https://github.com/pmd/pmd/pull/2938): \[cs] CPD: ignoring using directives could not be disabled -* java - * [#2911](https://github.com/pmd/pmd/issues/2911): \[java] `ClassTypeResolver#searchNodeNameForClass` leaks memory - * [#2934](https://github.com/pmd/pmd/pull/2934): \[java] CompareObjectsWithEquals / UseEqualsToCompareStrings - False negatives with fields - * [#2940](https://github.com/pmd/pmd/pull/2940): \[java] Catch additional TypeNotPresentExceptions / LinkageErrors -* scala - * [#2480](https://github.com/pmd/pmd/issues/2480): \[scala] Support CPD suppressions - - ### API Changes -#### Deprecated API - - -##### Around RuleSet parsing - -* {% jdoc core::RuleSetFactory %} and {% jdoc core::RuleSetFactoryUtils %} have been deprecated in favor of {% jdoc core::RuleSetLoader %}. This is easier to configure, and more maintainable than the multiple overloads of `RuleSetFactoryUtils`. -* Some static creation methods have been added to {% jdoc core::RuleSet %} for simple cases, eg {% jdoc core::RuleSet#forSingleRule(core::Rule) %}. These replace some counterparts in {% jdoc core::RuleSetFactory %} -* Since {% jdoc core::RuleSets %} is also deprecated, many APIs that require a RuleSets instance now are deprecated, and have a counterpart that expects a `List`. -* {% jdoc core::RuleSetReferenceId %}, {% jdoc core::RuleSetReference %}, {% jdoc core::RuleSetFactoryCompatibility %} are deprecated. They are most likely not relevant outside of the implementation of pmd-core. - -##### Around the `PMD` class - -Many classes around PMD's entry point ({% jdoc core::PMD %}) have been deprecated as internal, including: -* The contents of the packages {% jdoc_package core::cli %}, {% jdoc_package core::processor %} -* {% jdoc core::SourceCodeProcessor %} -* The constructors of {% jdoc core::PMD %} (the class will be made a utility class) - -##### Miscellaneous - -* {% jdoc !!java::lang.java.ast.ASTPackageDeclaration#getPackageNameImage() %}, - {% jdoc !!java::lang.java.ast.ASTTypeParameter#getParameterName() %} - and the corresponding XPath attributes. In both cases they're replaced with a new method `getName`, - the attribute is `@Name`. -* {% jdoc !!java::lang.java.ast.ASTClassOrInterfaceBody#isAnonymousInnerClass() %}, - and {% jdoc !!java::lang.java.ast.ASTClassOrInterfaceBody#isEnumChild() %}, - refs [#905](https://github.com/pmd/pmd/issues/905) - -#### Internal API - -Those APIs are not intended to be used by clients, and will be hidden or removed with PMD 7.0.0. -You can identify them with the `@InternalApi` annotation. You'll also get a deprecation warning. - -* {% jdoc !!javascript::lang.ecmascript.Ecmascript3Handler %} -* {% jdoc !!javascript::lang.ecmascript.Ecmascript3Parser %} -* {% jdoc !!javascript::lang.ecmascript.ast.EcmascriptParser#parserOptions %} -* {% jdoc !!javascript::lang.ecmascript.ast.EcmascriptParser#getSuppressMap() %} -* {% jdoc !!core::lang.rule.ParametricRuleViolation %} -* {% jdoc !!core::lang.ParserOptions#suppressMarker %} -* {% jdoc !!modelica::lang.modelica.rule.ModelicaRuleViolationFactory %} - - ### External Contributions -* [#2914](https://github.com/pmd/pmd/pull/2914): \[core] Include rule name in text renderer - [Gunther Schrijvers](https://github.com/GuntherSchrijvers) -* [#2925](https://github.com/pmd/pmd/pull/2925): Cleanup: Correct annotation array initializer indents from checkstyle #8083 - [Abhishek Kumar](https://github.com/Abhishek-kumar09) -* [#2929](https://github.com/pmd/pmd/pull/2929): \[scala] Add support for CPD-ON and CPD-OFF special comments - [Andy Robinson](https://github.com/andyrobinson) -* [#2936](https://github.com/pmd/pmd/pull/2936): \[java] (doc) Fix typo: "an accessor" not "a" - [Igor Moreno](https://github.com/igormoreno) -* [#2938](https://github.com/pmd/pmd/pull/2938): \[cs] CPD: fix issue where ignoring using directives could not be disabled - [Maikel Steneker](https://github.com/maikelsteneker) -* [#2945](https://github.com/pmd/pmd/pull/2945): \[cs] Add option to ignore sequences of literals - [Maikel Steneker](https://github.com/maikelsteneker) - {% endtocmaker %} + diff --git a/docs/pages/release_notes_old.md b/docs/pages/release_notes_old.md index 2e90ca3079..1736070850 100644 --- a/docs/pages/release_notes_old.md +++ b/docs/pages/release_notes_old.md @@ -5,6 +5,123 @@ permalink: pmd_release_notes_old.html Previous versions of PMD can be downloaded here: https://github.com/pmd/pmd/releases +## 12-December-2020 - 6.30.0 + +The PMD team is pleased to announce PMD 6.30.0. + +This is a minor release. + +### Table Of Contents + +* [New and noteworthy](#new-and-noteworthy) + * [CPD](#cpd) + * [Type information for VisualForce](#type-information-for-visualforce) +* [Fixed Issues](#fixed-issues) +* [API Changes](#api-changes) + * [Deprecated API](#deprecated-api) + * [Around RuleSet parsing](#around-ruleset-parsing) + * [Around the `PMD` class](#around-the-`pmd`-class) + * [Miscellaneous](#miscellaneous) + * [Internal API](#internal-api) +* [External Contributions](#external-contributions) +* [Stats](#stats) + +### New and noteworthy + +##### CPD + +* The C# module now supports the new option [`--ignore-literal-sequences`](https://pmd.github.io/latest/pmd_userdocs_cpd.html#-ignore-literal-sequences), which can be used to avoid detection of some uninteresting clones. Support for other languages may be added in the future. See [#2945](https://github.com/pmd/pmd/pull/2945) + +* The Scala module now supports [suppression](https://pmd.github.io/latest/pmd_userdocs_cpd.html#suppression) through `CPD-ON`/`CPD-OFF` comment pairs. See [#2929](https://github.com/pmd/pmd/pull/2929) + + +##### Type information for VisualForce + +The Visualforce AST now can resolve the data type of Visualforce expressions that reference Apex Controller properties and Custom Object fields. This feature improves the precision of existing rules, like [`VfUnescapeEl`](https://pmd.github.io/pmd-6.30.0/pmd_rules_vf_security.html#vfunescapeel). + +This can be configured using two environment variables: +* `PMD_VF_APEXDIRECTORIES`: Comma separated list of directories for Apex classes. Absolute or relative to the Visualforce directory. Default is `../classes`. Specifying an empty string will disable data type resolution for Apex Controller properties. +* `PMD_VF_OBJECTSDIRECTORIES`: Comma separated list of directories for Custom Objects. Absolute or relative to the Visualforce directory. Default is `../objects`. Specifying an empty string will disable data type resolution for Custom Object fields. + +This feature is experimental, in particular, expect changes to the way the configuration is specified. We'll probably extend the CLI instead of relying on environment variables in a future version. + +Thanks to Jeff Bartolotta and Roopa Mohan for contributing this! + +### Fixed Issues + +* core + * [#1939](https://github.com/pmd/pmd/issues/1939): \[core] XPath expressions return handling + * [#1961](https://github.com/pmd/pmd/issues/1961): \[core] Text renderer should include name of violated rule + * [#2874](https://github.com/pmd/pmd/pull/2874): \[core] Fix XMLRenderer with UTF-16 +* cs + * [#2938](https://github.com/pmd/pmd/pull/2938): \[cs] CPD: ignoring using directives could not be disabled +* java + * [#2911](https://github.com/pmd/pmd/issues/2911): \[java] `ClassTypeResolver#searchNodeNameForClass` leaks memory + * [#2934](https://github.com/pmd/pmd/pull/2934): \[java] CompareObjectsWithEquals / UseEqualsToCompareStrings - False negatives with fields + * [#2940](https://github.com/pmd/pmd/pull/2940): \[java] Catch additional TypeNotPresentExceptions / LinkageErrors +* scala + * [#2480](https://github.com/pmd/pmd/issues/2480): \[scala] Support CPD suppressions + + +### API Changes + +#### Deprecated API + + +##### Around RuleSet parsing + +* RuleSetFactory and RuleSetFactoryUtils have been deprecated in favor of RuleSetLoader. This is easier to configure, and more maintainable than the multiple overloads of `RuleSetFactoryUtils`. +* Some static creation methods have been added to RuleSet for simple cases, eg forSingleRule. These replace some counterparts in RuleSetFactory +* Since RuleSets is also deprecated, many APIs that require a RuleSets instance now are deprecated, and have a counterpart that expects a `List`. +* RuleSetReferenceId, RuleSetReference, RuleSetFactoryCompatibility are deprecated. They are most likely not relevant outside of the implementation of pmd-core. + +##### Around the `PMD` class + +Many classes around PMD's entry point (PMD) have been deprecated as internal, including: +* The contents of the packages net.sourceforge.pmd.cli, net.sourceforge.pmd.processor +* SourceCodeProcessor +* The constructors of PMD (the class will be made a utility class) + +##### Miscellaneous + +* ASTPackageDeclaration#getPackageNameImage, + ASTTypeParameter#getParameterName + and the corresponding XPath attributes. In both cases they're replaced with a new method `getName`, + the attribute is `@Name`. +* ASTClassOrInterfaceBody#isAnonymousInnerClass, + and ASTClassOrInterfaceBody#isEnumChild, + refs [#905](https://github.com/pmd/pmd/issues/905) + +#### Internal API + +Those APIs are not intended to be used by clients, and will be hidden or removed with PMD 7.0.0. +You can identify them with the `@InternalApi` annotation. You'll also get a deprecation warning. + +* net.sourceforge.pmd.lang.ecmascript.Ecmascript3Handler +* net.sourceforge.pmd.lang.ecmascript.Ecmascript3Parser +* EcmascriptParser#parserOptions +* EcmascriptParser#getSuppressMap +* net.sourceforge.pmd.lang.rule.ParametricRuleViolation +* ParserOptions#suppressMarker +* net.sourceforge.pmd.lang.modelica.rule.ModelicaRuleViolationFactory + + +### External Contributions + +* [#2864](https://github.com/pmd/pmd/pull/2864): [vf] Provide expression type information to Visualforce rules to avoid false positives - [Jeff Bartolotta](https://github.com/jbartolotta-sfdc) +* [#2914](https://github.com/pmd/pmd/pull/2914): \[core] Include rule name in text renderer - [Gunther Schrijvers](https://github.com/GuntherSchrijvers) +* [#2925](https://github.com/pmd/pmd/pull/2925): Cleanup: Correct annotation array initializer indents from checkstyle #8083 - [Abhishek Kumar](https://github.com/Abhishek-kumar09) +* [#2929](https://github.com/pmd/pmd/pull/2929): \[scala] Add support for CPD-ON and CPD-OFF special comments - [Andy Robinson](https://github.com/andyrobinson) +* [#2936](https://github.com/pmd/pmd/pull/2936): \[java] (doc) Fix typo: "an accessor" not "a" - [Igor Moreno](https://github.com/igormoreno) +* [#2938](https://github.com/pmd/pmd/pull/2938): \[cs] CPD: fix issue where ignoring using directives could not be disabled - [Maikel Steneker](https://github.com/maikelsteneker) +* [#2945](https://github.com/pmd/pmd/pull/2945): \[cs] Add option to ignore sequences of literals - [Maikel Steneker](https://github.com/maikelsteneker) +* [#2962](https://github.com/pmd/pmd/pull/2962): \[cpp] Add support for C++ 14 binary literals - [Maikel Steneker](https://github.com/maikelsteneker) + +### Stats +* 190 commits +* 25 closed tickets & PRs +* Days since last release: 49 + ## 24-October-2020 - 6.29.0 The PMD team is pleased to announce PMD 6.29.0. 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 e8f53a6d68..617dd1bd78 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 @@ -15,32 +15,29 @@ import org.junit.Test; import org.junit.contrib.java.lang.system.SystemErrRule; import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RuleSetFactory; import net.sourceforge.pmd.RuleSetLoader; public class DefaultRulesetTest { @Rule public final SystemErrRule systemErrRule = new SystemErrRule().enableLog().muteForSuccessfulTests(); - private RuleSetFactory factory = new RuleSetLoader().enableCompatibility(false).toFactory(); - @Test - public void loadDefaultRuleset() throws Exception { - RuleSet ruleset = factory.createRuleSet("rulesets/apex/ruleset.xml"); + public void loadDefaultRuleset() { + RuleSet ruleset = rulesetLoader().loadFromResource("rulesets/apex/ruleset.xml"); Assert.assertNotNull(ruleset); } @After public void cleanup() { - Handler[] handlers = Logger.getLogger(RuleSetFactory.class.getName()).getHandlers(); + Handler[] handlers = Logger.getLogger(RuleSetLoader.class.getName()).getHandlers(); for (Handler handler : handlers) { - Logger.getLogger(RuleSetFactory.class.getName()).removeHandler(handler); + Logger.getLogger(RuleSetLoader.class.getName()).removeHandler(handler); } } @Test - public void loadQuickstartRuleset() throws Exception { - Logger.getLogger(RuleSetFactory.class.getName()).addHandler(new Handler() { + public void loadQuickstartRuleset() { + Logger.getLogger(RuleSetLoader.class.getName()).addHandler(new Handler() { @Override public void publish(LogRecord record) { Assert.fail("No Logging expected: " + record.getMessage()); @@ -54,7 +51,11 @@ public class DefaultRulesetTest { public void close() throws SecurityException { } }); - RuleSet ruleset = factory.createRuleSet("rulesets/apex/quickstart.xml"); + RuleSet ruleset = rulesetLoader().loadFromResource("rulesets/apex/quickstart.xml"); Assert.assertNotNull(ruleset); } + + private RuleSetLoader rulesetLoader() { + return new RuleSetLoader().enableCompatibility(false); + } } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java b/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java index e089b479b6..5224cb7f38 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java @@ -219,13 +219,8 @@ public class PMD { public static int doPMD(final PMDConfiguration configuration) { // Load the RuleSets - final RuleSetFactory ruleSetFactory = - RuleSetLoader.fromPmdConfig(configuration).toFactory(); - final RuleSets ruleSets = - RulesetsFactoryUtils.getRuleSetsWithBenchmark(configuration.getRuleSets(), ruleSetFactory); - if (ruleSets == null) { - return PMDCommandLineInterface.NO_ERRORS_STATUS; - } + final RuleSetLoader ruleSetFactory = RuleSetLoader.fromPmdConfig(configuration); + final RuleSets ruleSets = new RuleSets(getRuleSetsWithBenchmark(configuration.getRuleSetPaths(), ruleSetFactory)); final Set languages = getApplicableLanguages(configuration, ruleSets); final List files = getApplicableFiles(configuration, languages); @@ -265,13 +260,52 @@ public class PMD { * Make sure it's our own classloader before attempting to close it.... * Maven + Jacoco provide us with a cloaseable classloader that if closed * will throw a ClassNotFoundException. - */ + */ if (configuration.getClassLoader() instanceof ClasspathClassLoader) { IOUtil.tryCloseClassLoader(configuration.getClassLoader()); } } } + private static List getRuleSetsWithBenchmark(List rulesetPaths, RuleSetLoader factory) { + try (TimedOperation to = TimeTracker.startOperation(TimedOperationCategory.LOAD_RULES)) { + List ruleSets; + try { + ruleSets = factory.loadFromResources(rulesetPaths); + printRuleNamesInDebug(ruleSets); + if (isEmpty(ruleSets)) { + String msg = "No rules found. Maybe you misspelled a rule name? (" + + String.join(",", rulesetPaths) + ')'; + LOG.log(Level.SEVERE, msg); + throw new IllegalArgumentException(msg); + } + } catch (RuleSetLoadException rsnfe) { + LOG.log(Level.SEVERE, "Ruleset not found", rsnfe); + throw rsnfe; + } + return ruleSets; + } + } + + private static boolean isEmpty(List rsets) { + return rsets.stream().noneMatch(it -> it.size() > 0); + } + + /** + * If in debug modus, print the names of the rules. + * + * @param rulesets the RuleSets to print + */ + private static void printRuleNamesInDebug(List rulesets) { + if (LOG.isLoggable(Level.FINER)) { + for (RuleSet rset : rulesets) { + for (Rule r : rset.getRules()) { + LOG.finer("Loaded rule " + r.getName()); + } + } + } + } + /** * Creates a new rule context, initialized with a new, empty report. * @@ -292,42 +326,6 @@ public class PMD { return context; } - /** - * Run PMD on a list of files using multiple threads - if more than one is - * available - * - * @param configuration - * Configuration - * @param ruleSetFactory - * RuleSetFactory - * @param files - * List of {@link DataSource}s - * @param ctx - * RuleContext - * @param renderers - * List of {@link Renderer}s - * - * @deprecated Use {@link #processFiles(PMDConfiguration, List, Collection, List)} - * so as not to depend on {@link RuleSetFactory}. Note that this sorts the list of data sources in-place, - * which won't be fixed - */ - @Deprecated - public static void processFiles(final PMDConfiguration configuration, final RuleSetFactory ruleSetFactory, - final List files, final RuleContext ctx, final List renderers) { - // Note that this duplicates the other routine, because the old behavior was - // that we parsed rulesets (a second time) inside the processor execution. - // To not mess up error handling, we keep this behavior. - - encourageToUseIncrementalAnalysis(configuration); - sortFiles(configuration, files); - // Make sure the cache is listening for analysis results - ctx.getReport().addListener(configuration.getAnalysisCache()); - - final RuleSetFactory silentFactory = ruleSetFactory.toLoader().warnDeprecated(false).toFactory(); - newFileProcessor(configuration).processFiles(silentFactory, files, ctx, renderers); - configuration.getAnalysisCache().persist(); - } - /** * Run PMD using the given configuration. This replaces the other overload. * 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 694519e344..79dafd3485 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/PMDConfiguration.java @@ -6,10 +6,14 @@ package net.sourceforge.pmd; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Properties; +import org.checkerframework.checker.nullness.qual.NonNull; + +import net.sourceforge.pmd.annotation.DeprecatedUntil700; import net.sourceforge.pmd.cache.AnalysisCache; import net.sourceforge.pmd.cache.FileAnalysisCache; import net.sourceforge.pmd.cache.NoopAnalysisCache; @@ -44,7 +48,7 @@ import net.sourceforge.pmd.util.ClasspathClassLoader; * *

The aspects related to Rules and Source files are:

*
    - *
  • A comma separated list of RuleSets URIs. {@link #getRuleSets()}
  • + *
  • RuleSets URIs: {@link #getRuleSetPaths()}
  • *
  • A minimum priority threshold when loading Rules from RuleSets, defaults * to {@link RulePriority#LOW}. {@link #getMinimumPriority()}
  • *
  • The character encoding of source files, defaults to the system default as @@ -53,7 +57,7 @@ import net.sourceforge.pmd.util.ClasspathClassLoader; *
  • A comma separated list of input paths to process for source files. This * may include files, directories, archives (e.g. ZIP files), etc. * {@link #getInputPaths()}
  • - *
  • A flag which controls, whether {@link RuleSetFactoryCompatibility} filter + *
  • A flag which controls, whether {@link RuleSetLoader#enableCompatibility(boolean)} filter * should be used or not: #isRuleSetFactoryCompatibilityEnabled; *
* @@ -86,7 +90,7 @@ public class PMDConfiguration extends AbstractConfiguration { private LanguageVersionDiscoverer languageVersionDiscoverer = new LanguageVersionDiscoverer(); // Rule and source file options - private String ruleSets; + private List ruleSets; private RulePriority minimumPriority = RulePriority.LOW; private String inputPaths; private String inputUri; @@ -258,19 +262,44 @@ public class PMDConfiguration extends AbstractConfiguration { * Get the comma separated list of RuleSet URIs. * * @return The RuleSet URIs. + * + * @deprecated Use {@link #getRuleSetPaths()} */ + @Deprecated + @DeprecatedUntil700 public String getRuleSets() { + return String.join(",", ruleSets); + } + + /** + * Returns the list of ruleset URIs. + * + * @see RuleSetLoader#loadFromResource(String) + */ + public List getRuleSetPaths() { return ruleSets; } + /** + * Sets the rulesets. + * + * @throws NullPointerException If the parameter is null + */ + public void setRuleSets(@NonNull List ruleSets) { + this.ruleSets = new ArrayList<>(ruleSets); + } + /** * Set the comma separated list of RuleSet URIs. * - * @param ruleSets - * the rulesets to set + * @param ruleSets the rulesets to set + * + * @deprecated Use {@link #setRuleSets(List)} */ + @Deprecated + @DeprecatedUntil700 public void setRuleSets(String ruleSets) { - this.ruleSets = ruleSets; + this.ruleSets = Arrays.asList(ruleSets.split(",")); } /** @@ -561,7 +590,7 @@ public class PMDConfiguration extends AbstractConfiguration { * * @return true, if the rule set factory compatibility feature is enabled * - * @see RuleSetFactoryCompatibility + * @see RuleSetLoader#enableCompatibility(boolean) */ public boolean isRuleSetFactoryCompatibilityEnabled() { return ruleSetFactoryCompatibilityEnabled; @@ -572,7 +601,7 @@ public class PMDConfiguration extends AbstractConfiguration { * * @param ruleSetFactoryCompatibilityEnabled {@code true} if the feature should be enabled * - * @see RuleSetFactoryCompatibility + * @see RuleSetLoader#enableCompatibility(boolean) */ public void setRuleSetFactoryCompatibilityEnabled(boolean ruleSetFactoryCompatibilityEnabled) { this.ruleSetFactoryCompatibilityEnabled = ruleSetFactoryCompatibilityEnabled; diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/Report.java b/pmd-core/src/main/java/net/sourceforge/pmd/Report.java index 68876112f2..ae72b12c6e 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/Report.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/Report.java @@ -231,11 +231,11 @@ public class Report { errors.addAll(r.errors); configErrors.addAll(r.configErrors); suppressedRuleViolations.addAll(r.suppressedRuleViolations); - } - for (RuleViolation violation : r.getViolations()) { - int index = Collections.binarySearch(violations, violation, RuleViolation.DEFAULT_COMPARATOR); - violations.add(index < 0 ? -index - 1 : index, violation); + for (RuleViolation violation : r.getViolations()) { + int index = Collections.binarySearch(violations, violation, RuleViolation.DEFAULT_COMPARATOR); + violations.add(index < 0 ? -index - 1 : index, violation); + } } } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSet.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSet.java index 06a3469b99..672a16b27d 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSet.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSet.java @@ -553,18 +553,6 @@ public class RuleSet implements ChecksumAware { } } - /** - * @deprecated Use {@link #getFileExclusions()} - */ - @Deprecated - public List getExcludePatterns() { - List excludes = new ArrayList<>(); - for (Pattern p : excludePatterns) { - excludes.add(p.pattern()); - } - return excludes; - } - /** * Returns the number of rules in this ruleset * @@ -722,18 +710,6 @@ public class RuleSet implements ChecksumAware { return description; } - /** - * @deprecated Use {@link #getFileInclusions()} - */ - @Deprecated - public List getIncludePatterns() { - List includes = new ArrayList<>(); - for (Pattern p : includePatterns) { - includes.add(p.pattern()); - } - return includes; - } - /** * Returns the file exclusion patterns as an unmodifiable list. */ 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 09a5f754b2..e3dbb3e702 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java @@ -7,10 +7,8 @@ package net.sourceforge.pmd; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; -import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -33,8 +31,6 @@ import org.xml.sax.InputSource; import org.xml.sax.SAXException; import net.sourceforge.pmd.RuleSet.RuleSetBuilder; -import net.sourceforge.pmd.lang.Language; -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; @@ -43,14 +39,10 @@ import net.sourceforge.pmd.util.ResourceLoader; * RuleSetFactory is responsible for creating RuleSet instances from XML * content. See {@link RuleSetLoader} for configuration options and * their defaults. - * - * @deprecated Use a {@link RuleSetLoader} instead. This will be hidden in PMD 7 - * (it's the implementation, while {@link RuleSetLoader} is the API). */ -@Deprecated -public class RuleSetFactory { +final class RuleSetFactory { - private static final Logger LOG = Logger.getLogger(RuleSetFactory.class.getName()); + private static final Logger LOG = Logger.getLogger(RuleSetLoader.class.getName()); private static final String DESCRIPTION = "description"; private static final String UNEXPECTED_ELEMENT = "Unexpected element <"; @@ -64,153 +56,19 @@ public class RuleSetFactory { private final Map parsedRulesets = new HashMap<>(); - /** - * @deprecated Use a {@link RuleSetLoader} to build a new factory - */ - @Deprecated // to be removed with PMD 7.0.0. - public RuleSetFactory() { - this(new ResourceLoader(), RulePriority.LOW, false, true); - } - - /** - * @deprecated Use a {@link RuleSetLoader} to build a new factory - */ - @Deprecated // to be removed with PMD 7.0.0. - public RuleSetFactory(final ClassLoader classLoader, final RulePriority minimumPriority, - final boolean warnDeprecated, final boolean enableCompatibility) { - this(new ResourceLoader(classLoader), minimumPriority, warnDeprecated, enableCompatibility); - } - - /** - * @deprecated Use a {@link RuleSetLoader} to build a new factory - */ - @Deprecated // to be hidden with PMD 7.0.0. - public RuleSetFactory(final ResourceLoader resourceLoader, final RulePriority minimumPriority, - final boolean warnDeprecated, final boolean enableCompatibility) { - this(resourceLoader, minimumPriority, warnDeprecated, enableCompatibility, false); - } - - RuleSetFactory(final ResourceLoader resourceLoader, final RulePriority minimumPriority, - final boolean warnDeprecated, final boolean enableCompatibility, boolean includeDeprecatedRuleReferences) { + RuleSetFactory(ResourceLoader resourceLoader, + RulePriority minimumPriority, + boolean warnDeprecated, + RuleSetFactoryCompatibility compatFilter, + boolean includeDeprecatedRuleReferences) { this.resourceLoader = resourceLoader; this.minimumPriority = minimumPriority; this.warnDeprecated = warnDeprecated; this.includeDeprecatedRuleReferences = includeDeprecatedRuleReferences; - if (enableCompatibility) { - this.compatibilityFilter = new RuleSetFactoryCompatibility(); - } else { - this.compatibilityFilter = null; - } + this.compatibilityFilter = compatFilter; } - /** - * Constructor copying all configuration from another factory. - * - * @param factory The factory whose configuration to copy. - * @param warnDeprecated Whether deprecation warnings are to be produced by this - * factory - * - * @deprecated Use {@link #toLoader()} to rebuild a factory from a configuration - */ - @Deprecated - public RuleSetFactory(final RuleSetFactory factory, final boolean warnDeprecated) { - this(factory.resourceLoader, factory.minimumPriority, warnDeprecated, factory.compatibilityFilter != null); - } - - - /** - * Gets the compatibility filter in order to adjust it, e.g. add additional - * filters. - * - * @return the {@link RuleSetFactoryCompatibility} - */ - /* package */ RuleSetFactoryCompatibility getCompatibilityFilter() { - return compatibilityFilter; - } - - /** - * Returns an Iterator of RuleSet objects loaded from descriptions from the - * "categories.properties" resource for each Language with Rule support. - * - * @return An Iterator of RuleSet objects. - * - * @throws RuleSetNotFoundException if the ruleset file could not be found - * - * @deprecated Use {@link RuleSetLoader#getStandardRuleSets()} - */ - @Deprecated - public Iterator getRegisteredRuleSets() throws RuleSetNotFoundException { - return toLoader().getStandardRuleSets().iterator(); - } - - /** - * Create a RuleSets from a comma separated list of RuleSet reference IDs. - * This is a convenience method which calls - * {@link RuleSetReferenceId#parse(String)}, and then calls - * {@link #createRuleSets(List)}. The currently configured ResourceLoader is - * used. - * - * @param referenceString - * A comma separated list of RuleSet reference IDs. - * @return The new RuleSets. - * @throws RuleSetNotFoundException - * if unable to find a resource. - * - * @deprecated Use {@link RuleSetLoader#loadFromResource(String)}, - * but note that that method does not split on commas - */ - @Deprecated - public RuleSets createRuleSets(String referenceString) throws RuleSetNotFoundException { - return createRuleSets(RuleSetReferenceId.parse(referenceString)); - } - - /** - * Create a RuleSets from a list of RuleSetReferenceIds. The currently - * configured ResourceLoader is used. - * - * @param ruleSetReferenceIds - * The List of RuleSetReferenceId of the RuleSets to create. - * @return The new RuleSets. - * @throws RuleSetNotFoundException - * if unable to find a resource. - * - * @deprecated Will not be replaced - */ - @Deprecated - public RuleSets createRuleSets(List ruleSetReferenceIds) throws RuleSetNotFoundException { - List ruleSets = new ArrayList<>(); - for (RuleSetReferenceId ruleSetReferenceId : ruleSetReferenceIds) { - RuleSet ruleSet = createRuleSet(ruleSetReferenceId); - ruleSets.add(ruleSet); - } - return new RuleSets(ruleSets); - } - - /** - * Create a RuleSet from a RuleSet reference ID string. This is a - * convenience method which calls {@link RuleSetReferenceId#parse(String)}, - * gets the first item in the List, and then calls - * {@link #createRuleSet(RuleSetReferenceId)}. The currently configured - * ResourceLoader is used. - * - * @param referenceString - * A comma separated list of RuleSet reference IDs. - * @return A new RuleSet. - * @throws RuleSetNotFoundException - * if unable to find a resource. - * - * @deprecated Use {@link RuleSetLoader#loadFromResource(String)} and discard the rest of the list. - */ - @Deprecated - public RuleSet createRuleSet(String referenceString) throws RuleSetNotFoundException { - List references = RuleSetReferenceId.parse(referenceString); - if (references.isEmpty()) { - throw new RuleSetNotFoundException( - "No RuleSetReferenceId can be parsed from the string: <" + referenceString + '>'); - } - return createRuleSet(references.get(0)); - } /** * Create a RuleSet from a RuleSetReferenceId. Priority filtering is ignored @@ -219,98 +77,16 @@ public class RuleSetFactory { * @param ruleSetReferenceId * The RuleSetReferenceId of the RuleSet to create. * @return A new RuleSet. - * @throws RuleSetNotFoundException - * if unable to find a resource. - * - * @deprecated Will not be replaced */ - @Deprecated - public RuleSet createRuleSet(RuleSetReferenceId ruleSetReferenceId) throws RuleSetNotFoundException { + RuleSet createRuleSet(RuleSetReferenceId ruleSetReferenceId) { return createRuleSet(ruleSetReferenceId, includeDeprecatedRuleReferences); } private RuleSet createRuleSet(RuleSetReferenceId ruleSetReferenceId, boolean withDeprecatedRuleReferences) - throws RuleSetNotFoundException { + throws RuleSetLoadException { return parseRuleSetNode(ruleSetReferenceId, withDeprecatedRuleReferences); } - /** - * Creates a copy of the given ruleset. All properties like name, description, fileName - * and exclude/include patterns are copied. - * - *

Note: The rule instances are shared between the original - * and the new ruleset (copy-by-reference). This might lead to concurrency issues, - * if the original ruleset and the new ruleset are used in different threads. - *

- * - * @param original the original rule set to copy from - * @return the copy - * - * @deprecated Use {@link RuleSet#copy(RuleSet)} - */ - @Deprecated - public RuleSet createRuleSetCopy(RuleSet original) { - RuleSetBuilder builder = new RuleSetBuilder(original); - return builder.build(); - } - - /** - * Creates a new ruleset with the given metadata such as name, description, - * fileName, exclude/include patterns are used. The rules are taken from the given - * collection. - * - *

Note: The rule instances are shared between the collection - * and the new ruleset (copy-by-reference). This might lead to concurrency issues, - * if the rules of the collection are also referenced by other rulesets and used - * in different threads. - *

- * - * @param name the name of the ruleset - * @param description the description - * @param fileName the filename - * @param excludePatterns list of exclude patterns, if any is not a valid regular expression, it will be ignored - * @param includePatterns list of include patterns, if any is not a valid regular expression, it will be ignored - * @param rules the collection with the rules to add to the new ruleset - * @return the new ruleset - * - * @deprecated Use {@link RuleSet#create(String, String, String, Collection, Collection, Iterable)} - */ - @Deprecated - public RuleSet createNewRuleSet(String name, - String description, - String fileName, - Collection excludePatterns, - Collection includePatterns, - Collection rules) { - return RuleSet.create(name, description, fileName, toPatterns(excludePatterns), toPatterns(includePatterns), rules); - } - - private Collection toPatterns(Collection sources) { - List result = new ArrayList<>(); - for (String s : sources) { - try { - result.add(Pattern.compile(s)); - } catch (PatternSyntaxException ignored) { - - } - } - return result; - } - - /** - * Creates a new RuleSet containing a single rule. - * - * @param rule The rule being created - * - * @return The newly created RuleSet - * - * @deprecated Use {@link RuleSet#forSingleRule(Rule)} - */ - @Deprecated - public RuleSet createSingleRuleRuleSet(final Rule rule) { - return RuleSet.forSingleRule(rule); - } - /** * Create a Rule from a RuleSet created from a file name resource. The * currently configured ResourceLoader is used. @@ -325,14 +101,11 @@ public class RuleSetFactory { * Whether RuleReferences that are deprecated should be ignored * or not * @return A new Rule. - * @throws RuleSetNotFoundException - * if unable to find a resource. */ - private Rule createRule(RuleSetReferenceId ruleSetReferenceId, boolean withDeprecatedRuleReferences) - throws RuleSetNotFoundException { + private Rule createRule(RuleSetReferenceId ruleSetReferenceId, boolean withDeprecatedRuleReferences) { if (ruleSetReferenceId.isAllRules()) { throw new IllegalArgumentException( - "Cannot parse a single Rule from an all Rule RuleSet reference: <" + ruleSetReferenceId + ">."); + "Cannot parse a single Rule from an all Rule RuleSet reference: <" + ruleSetReferenceId + ">."); } RuleSet ruleSet; // java8: computeIfAbsent @@ -348,28 +121,21 @@ public class RuleSetFactory { /** * 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 + * @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. */ - private RuleSet parseRuleSetNode(RuleSetReferenceId ruleSetReferenceId, boolean withDeprecatedRuleReferences) - throws RuleSetNotFoundException { + private RuleSet parseRuleSetNode(RuleSetReferenceId ruleSetReferenceId, boolean withDeprecatedRuleReferences) { try (CheckedInputStream inputStream = new CheckedInputStream( - ruleSetReferenceId.getInputStream(resourceLoader), new Adler32());) { + ruleSetReferenceId.getInputStream(resourceLoader), new Adler32());) { if (!ruleSetReferenceId.isExternal()) { throw new IllegalArgumentException( - "Cannot parse a RuleSet from a non-external reference: <" + ruleSetReferenceId + ">."); + "Cannot parse a RuleSet from a non-external reference: <" + ruleSetReferenceId + ">."); } DocumentBuilder builder = createDocumentBuilder(); - InputSource inputSource; - if (compatibilityFilter != null) { - inputSource = new InputSource(compatibilityFilter.filterRuleSetFile(inputStream)); - } else { - inputSource = new InputSource(inputStream); - } + InputSource inputSource = new InputSource(inputStream); Document document = builder.parse(inputSource); Element ruleSetElement = document.getDocumentElement(); @@ -492,10 +258,13 @@ public class RuleSetFactory { * @param rulesetReferences keeps track of already processed complete ruleset references in order to log a warning */ private void parseRuleNode(RuleSetReferenceId ruleSetReferenceId, RuleSetBuilder ruleSetBuilder, Node ruleNode, - boolean withDeprecatedRuleReferences, Set rulesetReferences) - throws RuleSetNotFoundException { + boolean withDeprecatedRuleReferences, Set rulesetReferences) { Element ruleElement = (Element) ruleNode; String ref = ruleElement.getAttribute("ref"); + ref = compatibilityFilter.applyRef(ref, this.warnDeprecated); + if (ref == null) { + return; // deleted rule + } if (ref.endsWith("xml")) { parseRuleSetReferenceNode(ruleSetBuilder, ruleElement, ref, rulesetReferences); } else if (StringUtils.isBlank(ref)) { @@ -519,8 +288,7 @@ public class RuleSetFactory { * 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, Set rulesetReferences) - throws RuleSetNotFoundException { + private void parseRuleSetReferenceNode(RuleSetBuilder ruleSetBuilder, Element ruleElement, String ref, Set rulesetReferences) { String priority = null; NodeList childNodes = ruleElement.getChildNodes(); Set excludedRulesCheck = new HashSet<>(); @@ -529,7 +297,10 @@ public class RuleSetFactory { if (isElementNode(child, "exclude")) { Element excludeElement = (Element) child; String excludedRuleName = excludeElement.getAttribute("name"); - excludedRulesCheck.add(excludedRuleName); + excludedRuleName = compatibilityFilter.applyExclude(ref, excludedRuleName, this.warnDeprecated); + if (excludedRuleName != null) { + excludedRulesCheck.add(excludedRuleName); + } } else if (isElementNode(child, PRIORITY)) { priority = parseTextNode(child).trim(); } @@ -641,13 +412,13 @@ public class RuleSetFactory { * or not */ private void parseRuleReferenceNode(RuleSetReferenceId ruleSetReferenceId, RuleSetBuilder ruleSetBuilder, - Node ruleNode, String ref, boolean withDeprecatedRuleReferences) throws RuleSetNotFoundException { + Node ruleNode, String ref, boolean withDeprecatedRuleReferences) { Element ruleElement = (Element) ruleNode; // Stop if we're looking for a particular Rule, and this element is not // it. if (StringUtils.isNotBlank(ruleSetReferenceId.getRuleName()) - && !isRuleName(ruleElement, ruleSetReferenceId.getRuleName())) { + && !isRuleName(ruleElement, ruleSetReferenceId.getRuleName())) { return; } @@ -751,7 +522,7 @@ public class RuleSetFactory { } } } catch (Exception e) { - throw new RuntimeException(e); + throw new RuleSetLoadException("Cannot load " + ruleSetReferenceId, e); } return found; diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java index 6ed1d189a4..d730065b47 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java @@ -4,21 +4,13 @@ package net.sourceforge.pmd; -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; -import java.nio.charset.StandardCharsets; -import java.util.LinkedList; +import java.text.MessageFormat; +import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.apache.commons.io.IOUtils; - -import net.sourceforge.pmd.annotation.InternalApi; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Provides a simple filter mechanism to avoid failing to parse an old ruleset, @@ -26,72 +18,72 @@ import net.sourceforge.pmd.annotation.InternalApi; * renamed or moved to another ruleset. * * @see issue 1360 - * - * @deprecated Use {@link RuleSetLoader#enableCompatibility(boolean)} to enable this feature. - * This implementation is internal API. */ -@InternalApi -@Deprecated -public class RuleSetFactoryCompatibility { - private static final Logger LOG = Logger.getLogger(RuleSetFactoryCompatibility.class.getName()); +final class RuleSetFactoryCompatibility { - private List filters = new LinkedList<>(); + static final RuleSetFactoryCompatibility EMPTY = new RuleSetFactoryCompatibility(); + /** The instance with the built-in filters for the modified PMD rules. */ + static final RuleSetFactoryCompatibility DEFAULT = new RuleSetFactoryCompatibility(); - /** - * Creates a new instance of the compatibility filter with the built-in - * filters for the modified PMD rules. - */ - public RuleSetFactoryCompatibility() { + + static { // PMD 5.3.0 - addFilterRuleRenamed("java", "design", "UncommentedEmptyMethod", "UncommentedEmptyMethodBody"); - addFilterRuleRemoved("java", "controversial", "BooleanInversion"); + DEFAULT.addFilterRuleRenamed("java", "design", "UncommentedEmptyMethod", "UncommentedEmptyMethodBody"); + DEFAULT.addFilterRuleRemoved("java", "controversial", "BooleanInversion"); // PMD 5.3.1 - addFilterRuleRenamed("java", "design", "UseSingleton", "UseUtilityClass"); + DEFAULT.addFilterRuleRenamed("java", "design", "UseSingleton", "UseUtilityClass"); // PMD 5.4.0 - addFilterRuleMoved("java", "basic", "empty", "EmptyCatchBlock"); - addFilterRuleMoved("java", "basic", "empty", "EmptyIfStatement"); - addFilterRuleMoved("java", "basic", "empty", "EmptyWhileStmt"); - addFilterRuleMoved("java", "basic", "empty", "EmptyTryBlock"); - addFilterRuleMoved("java", "basic", "empty", "EmptyFinallyBlock"); - addFilterRuleMoved("java", "basic", "empty", "EmptySwitchStatements"); - addFilterRuleMoved("java", "basic", "empty", "EmptySynchronizedBlock"); - addFilterRuleMoved("java", "basic", "empty", "EmptyStatementNotInLoop"); - addFilterRuleMoved("java", "basic", "empty", "EmptyInitializer"); - addFilterRuleMoved("java", "basic", "empty", "EmptyStatementBlock"); - addFilterRuleMoved("java", "basic", "empty", "EmptyStaticInitializer"); - addFilterRuleMoved("java", "basic", "unnecessary", "UnnecessaryConversionTemporary"); - addFilterRuleMoved("java", "basic", "unnecessary", "UnnecessaryReturn"); - addFilterRuleMoved("java", "basic", "unnecessary", "UnnecessaryFinalModifier"); - addFilterRuleMoved("java", "basic", "unnecessary", "UselessOverridingMethod"); - addFilterRuleMoved("java", "basic", "unnecessary", "UselessOperationOnImmutable"); - addFilterRuleMoved("java", "basic", "unnecessary", "UnusedNullCheckInEquals"); - addFilterRuleMoved("java", "basic", "unnecessary", "UselessParentheses"); + DEFAULT.addFilterRuleMoved("java", "basic", "empty", "EmptyCatchBlock"); + DEFAULT.addFilterRuleMoved("java", "basic", "empty", "EmptyIfStatement"); + DEFAULT.addFilterRuleMoved("java", "basic", "empty", "EmptyWhileStmt"); + DEFAULT.addFilterRuleMoved("java", "basic", "empty", "EmptyTryBlock"); + DEFAULT.addFilterRuleMoved("java", "basic", "empty", "EmptyFinallyBlock"); + DEFAULT.addFilterRuleMoved("java", "basic", "empty", "EmptySwitchStatements"); + DEFAULT.addFilterRuleMoved("java", "basic", "empty", "EmptySynchronizedBlock"); + DEFAULT.addFilterRuleMoved("java", "basic", "empty", "EmptyStatementNotInLoop"); + DEFAULT.addFilterRuleMoved("java", "basic", "empty", "EmptyInitializer"); + DEFAULT.addFilterRuleMoved("java", "basic", "empty", "EmptyStatementBlock"); + DEFAULT.addFilterRuleMoved("java", "basic", "empty", "EmptyStaticInitializer"); + DEFAULT.addFilterRuleMoved("java", "basic", "unnecessary", "UnnecessaryConversionTemporary"); + DEFAULT.addFilterRuleMoved("java", "basic", "unnecessary", "UnnecessaryReturn"); + DEFAULT.addFilterRuleMoved("java", "basic", "unnecessary", "UnnecessaryFinalModifier"); + DEFAULT.addFilterRuleMoved("java", "basic", "unnecessary", "UselessOverridingMethod"); + DEFAULT.addFilterRuleMoved("java", "basic", "unnecessary", "UselessOperationOnImmutable"); + DEFAULT.addFilterRuleMoved("java", "basic", "unnecessary", "UnusedNullCheckInEquals"); + DEFAULT.addFilterRuleMoved("java", "basic", "unnecessary", "UselessParentheses"); // PMD 5.6.0 - addFilterRuleRenamed("java", "design", "AvoidConstantsInterface", "ConstantsInInterface"); + DEFAULT.addFilterRuleRenamed("java", "design", "AvoidConstantsInterface", "ConstantsInInterface"); // unused/UnusedModifier moved AND renamed, order is important! - addFilterRuleMovedAndRenamed("java", "unusedcode", "UnusedModifier", "unnecessary", "UnnecessaryModifier"); + DEFAULT.addFilterRuleMovedAndRenamed("java", "unusedcode", "UnusedModifier", "unnecessary", "UnnecessaryModifier"); // PMD 6.0.0 - addFilterRuleMoved("java", "controversial", "unnecessary", "UnnecessaryParentheses"); - addFilterRuleRenamed("java", "unnecessary", "UnnecessaryParentheses", "UselessParentheses"); - addFilterRuleMoved("java", "typeresolution", "coupling", "LooseCoupling"); - addFilterRuleMoved("java", "typeresolution", "clone", "CloneMethodMustImplementCloneable"); - addFilterRuleMoved("java", "typeresolution", "imports", "UnusedImports"); - addFilterRuleMoved("java", "typeresolution", "strictexception", "SignatureDeclareThrowsException"); - addFilterRuleRenamed("java", "naming", "MisleadingVariableName", "MIsLeadingVariableName"); - addFilterRuleRenamed("java", "unnecessary", "UnnecessaryFinalModifier", "UnnecessaryModifier"); - addFilterRuleRenamed("java", "empty", "EmptyStaticInitializer", "EmptyInitializer"); + DEFAULT.addFilterRuleMoved("java", "controversial", "unnecessary", "UnnecessaryParentheses"); + DEFAULT.addFilterRuleRenamed("java", "unnecessary", "UnnecessaryParentheses", "UselessParentheses"); + DEFAULT.addFilterRuleMoved("java", "typeresolution", "coupling", "LooseCoupling"); + DEFAULT.addFilterRuleMoved("java", "typeresolution", "clone", "CloneMethodMustImplementCloneable"); + DEFAULT.addFilterRuleMoved("java", "typeresolution", "imports", "UnusedImports"); + DEFAULT.addFilterRuleMoved("java", "typeresolution", "strictexception", "SignatureDeclareThrowsException"); + DEFAULT.addFilterRuleRenamed("java", "naming", "MisleadingVariableName", "MIsLeadingVariableName"); + DEFAULT.addFilterRuleRenamed("java", "unnecessary", "UnnecessaryFinalModifier", "UnnecessaryModifier"); + DEFAULT.addFilterRuleRenamed("java", "empty", "EmptyStaticInitializer", "EmptyInitializer"); // GuardLogStatementJavaUtil moved and renamed... - addFilterRuleMovedAndRenamed("java", "logging-java", "GuardLogStatementJavaUtil", "logging-jakarta-commons", "GuardLogStatement"); - addFilterRuleRenamed("java", "logging-jakarta-commons", "GuardDebugLogging", "GuardLogStatement"); + DEFAULT.addFilterRuleMovedAndRenamed("java", "logging-java", "GuardLogStatementJavaUtil", "logging-jakarta-commons", "GuardLogStatement"); + DEFAULT.addFilterRuleRenamed("java", "logging-jakarta-commons", "GuardDebugLogging", "GuardLogStatement"); + } + + private static final Logger LOG = Logger.getLogger(RuleSetFactoryCompatibility.class.getName()); + + private final List filters = new ArrayList<>(); + + void addFilterRuleMovedAndRenamed(String language, String oldRuleset, String oldName, String newRuleset, String newName) { filters.add(RuleSetFilter.ruleMoved(language, oldRuleset, newRuleset, oldName)); - filters.add(RuleSetFilter.ruleRenamedMoved(language, newRuleset, oldName, newName)); + filters.add(RuleSetFilter.ruleRenamed(language, newRuleset, oldName, newName)); } void addFilterRuleRenamed(String language, String ruleset, String oldName, String newName) { @@ -106,134 +98,130 @@ public class RuleSetFactoryCompatibility { filters.add(RuleSetFilter.ruleRemoved(language, ruleset, name)); } - /** - * Applies all configured filters against the given input stream. The - * resulting reader will contain the original ruleset modified by the - * filters. - * - * @param stream the original ruleset file input stream - * @return a reader, from which the filtered ruleset can be read - * @throws IOException if the stream couldn't be read - */ - public Reader filterRuleSetFile(InputStream stream) throws IOException { - byte[] bytes = IOUtils.toByteArray(stream); - String encoding = determineEncoding(bytes); - String ruleset = new String(bytes, encoding); - - ruleset = applyAllFilters(ruleset); - - return new StringReader(ruleset); + @Nullable String applyRef(String ref) { + return applyRef(ref, false); } - private String applyAllFilters(String ruleset) { - String result = ruleset; + + /** + * Returns the new rule ref, or null if the rule was deleted. Returns + * the argument if no replacement is needed. + * + * @param ref Original ref + * @param warn Whether to output a warning if a replacement is done + */ + public @Nullable String applyRef(String ref, boolean warn) { + String result = ref; for (RuleSetFilter filter : filters) { - result = filter.apply(result); + result = filter.applyRef(result, warn); + if (result == null) { + return null; + } } return result; } - private static final Pattern ENCODING_PATTERN = Pattern.compile("encoding=\"([^\"]+)\""); - /** - * Determines the encoding of the given bytes, assuming this is a XML - * document, which specifies the encoding in the first 1024 bytes. + * Returns the new rule name, or null if the rule was deleted. Returns + * the argument if no replacement is needed. * - * @param bytes - * the input bytes, might be more or less than 1024 bytes - * @return the determined encoding, falls back to the default UTF-8 encoding + * @param rulesetRef Ruleset name + * @param excludeName Original excluded name + * @param warn Whether to output a warning if a replacement is done */ - String determineEncoding(byte[] bytes) { - String firstBytes = new String(bytes, 0, bytes.length > 1024 ? 1024 : bytes.length, - StandardCharsets.ISO_8859_1); - Matcher matcher = ENCODING_PATTERN.matcher(firstBytes); - String encoding = StandardCharsets.UTF_8.name(); - if (matcher.find()) { - encoding = matcher.group(1); + public @Nullable String applyExclude(String rulesetRef, String excludeName, boolean warn) { + String result = excludeName; + for (RuleSetFilter filter : filters) { + result = filter.applyExclude(rulesetRef, result, warn); + if (result == null) { + return null; + } } - return encoding; + return result; } private static class RuleSetFilter { - private final Pattern refPattern; - private final String replacement; - private Pattern exclusionPattern; - private String exclusionReplacement; + + private static final String MOVED_MESSAGE = "The rule \"{1}\" has been moved from ruleset \"{0}\" to \"{2}\". Please change your ruleset!"; + private static final String RENAMED_MESSAGE = "The rule \"{1}\" has been renamed to \"{3}\". Please change your ruleset!"; + private static final String REMOVED_MESSAGE = "The rule \"{1}\" in ruleset \"{0}\" has been removed from PMD and no longer exists. Please change your ruleset!"; + private final String ruleRef; + private final String oldRuleset; + private final String oldName; + private final String newRuleset; + private final String newName; private final String logMessage; - private RuleSetFilter(String refPattern, String replacement, String logMessage) { + private RuleSetFilter(String oldRuleset, + String oldName, + @Nullable String newRuleset, + @Nullable String newName, + String logMessage) { + this.oldRuleset = oldRuleset; + this.oldName = oldName; + this.newRuleset = newRuleset; + this.newName = newName; this.logMessage = logMessage; - if (replacement != null) { - this.refPattern = Pattern.compile("ref=\"" + Pattern.quote(refPattern) + "\""); - this.replacement = "ref=\"" + replacement + "\""; - } else { - this.refPattern = Pattern.compile(""); - this.replacement = ""; - } - } - - private void setExclusionPattern(String oldName, String newName) { - exclusionPattern = Pattern.compile(""); - if (newName != null) { - exclusionReplacement = ""; - } else { - exclusionReplacement = ""; - } + this.ruleRef = oldRuleset + "/" + oldName; } public static RuleSetFilter ruleRenamed(String language, String ruleset, String oldName, String newName) { - RuleSetFilter filter = ruleRenamedMoved(language, ruleset, oldName, newName); - filter.setExclusionPattern(oldName, newName); - return filter; - } - - public static RuleSetFilter ruleRenamedMoved(String language, String ruleset, String oldName, String newName) { - String base = "rulesets/" + language + "/" + ruleset + ".xml/"; - return new RuleSetFilter(base + oldName, base + newName, "The rule \"" + oldName - + "\" has been renamed to \"" + newName + "\". Please change your ruleset!"); + String base = "rulesets/" + language + "/" + ruleset + ".xml"; + return new RuleSetFilter(base, oldName, base, newName, RENAMED_MESSAGE); } public static RuleSetFilter ruleMoved(String language, String oldRuleset, String newRuleset, String ruleName) { String base = "rulesets/" + language + "/"; - return new RuleSetFilter(base + oldRuleset + ".xml/" + ruleName, base + newRuleset + ".xml/" + ruleName, - "The rule \"" + ruleName + "\" has been moved from ruleset \"" + oldRuleset + "\" to \"" - + newRuleset + "\". Please change your ruleset!"); + return new RuleSetFilter(base + oldRuleset + ".xml", ruleName, + base + newRuleset + ".xml", ruleName, + MOVED_MESSAGE); } public static RuleSetFilter ruleRemoved(String language, String ruleset, String name) { - RuleSetFilter filter = new RuleSetFilter("rulesets/" + language + "/" + ruleset + ".xml/" + name, null, - "The rule \"" + name + "\" in ruleset \"" + ruleset - + "\" has been removed from PMD and no longer exists. Please change your ruleset!"); - filter.setExclusionPattern(name, null); - return filter; + String oldRuleset = "rulesets/" + language + "/" + ruleset + ".xml"; + return new RuleSetFilter(oldRuleset, name, + null, null, + REMOVED_MESSAGE); } - String apply(String ruleset) { - String result = ruleset; - Matcher matcher = refPattern.matcher(ruleset); + @Nullable String applyExclude(String ref, String name, boolean warn) { + if (oldRuleset.equals(ref) + && oldName.equals(name) + && oldRuleset.equals(newRuleset)) { + if (warn) { + warn(); + } - if (matcher.find()) { - result = matcher.replaceAll(replacement); + return newName; + } - if (LOG.isLoggable(Level.WARNING)) { - LOG.warning("Applying rule set filter: " + logMessage); + return name; + } + + @Nullable String applyRef(String ref, boolean warn) { + + if (ref.equals(this.ruleRef)) { + + if (warn) { + warn(); + } + + if (newName != null) { + return newRuleset + "/" + newName; + } else { + // deleted + return null; } } - if (exclusionPattern == null) { - return result; + return ref; + } + + private void warn() { + if (LOG.isLoggable(Level.WARNING)) { + String log = MessageFormat.format(logMessage, oldRuleset, oldName, newRuleset, newName); + LOG.warning("Applying rule set filter: " + log); } - - Matcher exclusions = exclusionPattern.matcher(result); - if (exclusions.find()) { - result = exclusions.replaceAll(exclusionReplacement); - - if (LOG.isLoggable(Level.WARNING)) { - LOG.warning("Applying rule set filter for exclusions: " + logMessage); - } - } - - return result; } } } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RulesetLoadException.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoadException.java similarity index 68% rename from pmd-core/src/main/java/net/sourceforge/pmd/RulesetLoadException.java rename to pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoadException.java index 7979971118..4fa14bcb62 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RulesetLoadException.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoadException.java @@ -11,21 +11,18 @@ import net.sourceforge.pmd.annotation.InternalApi; * {@linkplain RuleSetLoader loading rulesets}. This may be because the * XML is not well-formed, does not respect the ruleset schema, is * not a valid ruleset or is otherwise unparsable. - * - *

In the new {@link RuleSetLoader} API, this is thrown instead of - * {@link RuleSetNotFoundException}. */ -public final class RulesetLoadException extends RuntimeException { +public final class RuleSetLoadException extends RuntimeException { /** Constructors are internal. */ @InternalApi - public RulesetLoadException(String message, Throwable cause) { + public RuleSetLoadException(String message, Throwable cause) { super(message, cause); } /** Constructors are internal. */ @InternalApi - public RulesetLoadException(String message) { + public RuleSetLoadException(String message) { super(message); } 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 a6f16fa44f..1aee4342b5 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java @@ -4,14 +4,17 @@ 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.Properties; -import java.util.logging.Level; -import java.util.logging.Logger; + +import org.checkerframework.checker.nullness.qual.NonNull; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageRegistry; @@ -26,12 +29,10 @@ import net.sourceforge.pmd.util.ResourceLoader; */ public final class RuleSetLoader { - private static final Logger LOG = Logger.getLogger(RuleSetLoader.class.getName()); - private ResourceLoader resourceLoader = new ResourceLoader(RuleSetLoader.class.getClassLoader()); private RulePriority minimumPriority = RulePriority.LOW; private boolean warnDeprecated = true; - private boolean enableCompatibility = true; + private @NonNull RuleSetFactoryCompatibility compatFilter = RuleSetFactoryCompatibility.DEFAULT; private boolean includeDeprecatedRuleReferences = false; /** @@ -82,7 +83,13 @@ public final class RuleSetLoader { * @return This instance, modified */ public RuleSetLoader enableCompatibility(boolean enable) { - this.enableCompatibility = enable; + return setCompatibility(enable ? RuleSetFactoryCompatibility.DEFAULT + : RuleSetFactoryCompatibility.EMPTY); + } + + // test only + RuleSetLoader setCompatibility(@NonNull RuleSetFactoryCompatibility filter) { + this.compatFilter = filter; return this; } @@ -111,7 +118,7 @@ public final class RuleSetLoader { this.resourceLoader, this.minimumPriority, this.warnDeprecated, - this.enableCompatibility, + this.compatFilter, this.includeDeprecatedRuleReferences ); } @@ -121,23 +128,37 @@ public final class RuleSetLoader { * Parses and returns a ruleset from its location. The location may * be a file system path, or a resource path (see {@link #loadResourcesWith(ClassLoader)}). * - *

This replaces {@link RuleSetFactory#createRuleSet(String)}, - * but does not split commas. - * * @param rulesetPath A reference to a single ruleset * - * @throws RulesetLoadException If any error occurs (eg, invalid syntax, or resource not found) + * @throws RuleSetLoadException If any error occurs (eg, invalid syntax, or resource not found) */ public RuleSet loadFromResource(String rulesetPath) { return loadFromResource(new RuleSetReferenceId(rulesetPath)); } + /** + * 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, String rulesetXmlContent) { + return loadFromResource(new RuleSetReferenceId(filename) { + @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), + * @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 */ @@ -155,7 +176,7 @@ public final class RuleSetLoader { * @param first First path * @param rest Paths * - * @throws RulesetLoadException If any error occurs (eg, invalid syntax, or resource not found), + * @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 */ @@ -168,7 +189,7 @@ public final class RuleSetLoader { try { return toFactory().createRuleSet(ruleSetReferenceId); } catch (Exception e) { - throw new RulesetLoadException("Cannot parse " + ruleSetReferenceId, e); + throw new RuleSetLoadException("Cannot parse " + ruleSetReferenceId, e); } } @@ -190,14 +211,14 @@ public final class RuleSetLoader { * * @return A list of all category rulesets * - * @throws RulesetLoadException If a standard ruleset cannot be loaded. + * @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<>(); + List ruleSetReferenceIds = new ArrayList<>(); for (Language language : LanguageRegistry.getLanguages()) { Properties props = new Properties(); rulesetsProperties = "category/" + language.getTerseName() + "/categories.properties"; @@ -205,24 +226,17 @@ public final class RuleSetLoader { props.load(inputStream); String rulesetFilenames = props.getProperty("rulesets.filenames"); if (rulesetFilenames != null) { - ruleSetReferenceIds.addAll(RuleSetReferenceId.parse(rulesetFilenames)); - } - } catch (RuleSetNotFoundException e) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("The language " + language.getTerseName() + " provides no " + rulesetsProperties + "."); - } - } catch (IOException ioe) { - if (LOG.isLoggable(Level.FINE)) { - LOG.fine("Couldn't read " + rulesetsProperties - + "; please ensure that the directory is on the classpath. The current classpath is: " - + System.getProperty("java.class.path")); - LOG.fine(ioe.toString()); + 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 (RuleSetReferenceId id : ruleSetReferenceIds) { + for (String id : ruleSetReferenceIds) { ruleSets.add(loadFromResource(id)); // may throw } return ruleSets; diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetNotFoundException.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetNotFoundException.java deleted file mode 100644 index 05dd3d4be2..0000000000 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetNotFoundException.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * BSD-style license; for more info see http://pmd.sourceforge.net/license.html - */ - -package net.sourceforge.pmd; - -/** - * @deprecated This is now only thrown by deprecated apis. {@link RuleSetLoader} - * throws {@link RulesetLoadException} instead - */ -@Deprecated -public class RuleSetNotFoundException extends Exception { - - private static final long serialVersionUID = -4617033110919250810L; - - public RuleSetNotFoundException(String msg) { - super(msg); - } - - public RuleSetNotFoundException(String msg, Throwable cause) { - super(msg, cause); - } -} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetReferenceId.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetReferenceId.java index 66dd5e47c9..d7ade89948 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetReferenceId.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetReferenceId.java @@ -5,6 +5,7 @@ package net.sourceforge.pmd; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; @@ -397,24 +398,29 @@ public class RuleSetReferenceId { * * @param rl The {@link ResourceLoader} to use. * @return An InputStream to that resource. - * @throws RuleSetNotFoundException - * if unable to find a resource. */ - public InputStream getInputStream(final ResourceLoader rl) throws RuleSetNotFoundException { + public InputStream getInputStream(final ResourceLoader rl) throws IOException { if (externalRuleSetReferenceId == null) { - InputStream in = StringUtils.isBlank(ruleSetFileName) ? null - : rl.loadResourceAsStream(ruleSetFileName); - if (in == null) { - throw new RuleSetNotFoundException("Can't find resource '" + ruleSetFileName + "' for rule '" + ruleName - + "'" + ". Make sure the resource is a valid file or URL and is on the CLASSPATH. " - + "Here's the current classpath: " + System.getProperty("java.class.path")); + if (StringUtils.isBlank(ruleSetFileName)) { + throw notFoundException(); + } + try { + return rl.loadResourceAsStream(ruleSetFileName); + } catch (FileNotFoundException ignored) { + throw notFoundException(); } - return in; } else { return externalRuleSetReferenceId.getInputStream(rl); } } + private FileNotFoundException notFoundException() { + return new FileNotFoundException("Can't find resource '" + ruleSetFileName + "' for rule '" + ruleName + + "'" + ". Make sure the resource is a valid file or URL and is on the classpath. " + + "Here's the current classpath: " + + System.getProperty("java.class.path")); + } + /** * Return the String form of this Rule reference. * diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetWriter.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetWriter.java index 4719748ad0..126d768b3a 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetWriter.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetWriter.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; @@ -104,12 +105,12 @@ public class RuleSetWriter { Element descriptionElement = createDescriptionElement(ruleSet.getDescription()); ruleSetElement.appendChild(descriptionElement); - for (String excludePattern : ruleSet.getExcludePatterns()) { - Element excludePatternElement = createExcludePatternElement(excludePattern); + for (Pattern excludePattern : ruleSet.getFileExclusions()) { + Element excludePatternElement = createExcludePatternElement(excludePattern.pattern()); ruleSetElement.appendChild(excludePatternElement); } - for (String includePattern : ruleSet.getIncludePatterns()) { - Element includePatternElement = createIncludePatternElement(includePattern); + for (Pattern includePattern : ruleSet.getFileInclusions()) { + Element includePatternElement = createIncludePatternElement(includePattern.pattern()); ruleSetElement.appendChild(includePatternElement); } for (Rule rule : ruleSet.getRules()) { diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RulesetsFactoryUtils.java b/pmd-core/src/main/java/net/sourceforge/pmd/RulesetsFactoryUtils.java deleted file mode 100644 index 135736f8a6..0000000000 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RulesetsFactoryUtils.java +++ /dev/null @@ -1,232 +0,0 @@ -/** - * BSD-style license; for more info see http://pmd.sourceforge.net/license.html - */ - -package net.sourceforge.pmd; - -import java.util.logging.Level; -import java.util.logging.Logger; - -import net.sourceforge.pmd.annotation.InternalApi; -import net.sourceforge.pmd.benchmark.TimeTracker; -import net.sourceforge.pmd.benchmark.TimedOperation; -import net.sourceforge.pmd.benchmark.TimedOperationCategory; -import net.sourceforge.pmd.util.ResourceLoader; - -/** - * @deprecated Use a {@link RuleSetLoader} instead - */ -@Deprecated -public final class RulesetsFactoryUtils { - - private static final Logger LOG = Logger.getLogger(RulesetsFactoryUtils.class.getName()); - - private RulesetsFactoryUtils() { - } - - /** - * Creates a new rulesets with the given string. The resulting rulesets will - * contain all referenced rulesets. - * - * @param rulesets - * the string with the rulesets to load - * @param factory - * the ruleset factory - * @return the rulesets - * @throws IllegalArgumentException - * if rulesets is empty (means, no rules have been found) or if - * a ruleset couldn't be found. - * @deprecated Internal API - */ - @InternalApi - @Deprecated - public static RuleSets getRuleSets(String rulesets, RuleSetFactory factory) { - RuleSets ruleSets = null; - try { - ruleSets = factory.createRuleSets(rulesets); - printRuleNamesInDebug(ruleSets); - if (ruleSets.ruleCount() == 0) { - String msg = "No rules found. Maybe you misspelled a rule name? (" + rulesets + ')'; - LOG.log(Level.SEVERE, msg); - throw new IllegalArgumentException(msg); - } - } catch (RuleSetNotFoundException rsnfe) { - LOG.log(Level.SEVERE, "Ruleset not found", rsnfe); - throw new IllegalArgumentException(rsnfe); - } - return ruleSets; - } - - /** - * See {@link #getRuleSets(String, RuleSetFactory)}. In addition, the - * loading of the rules is benchmarked. - * - * @param rulesets - * the string with the rulesets to load - * @param factory - * the ruleset factory - * @return the rulesets - * @throws IllegalArgumentException - * if rulesets is empty (means, no rules have been found) or if - * a ruleset couldn't be found. - * @deprecated Is internal API - */ - @InternalApi - @Deprecated - public static RuleSets getRuleSetsWithBenchmark(String rulesets, RuleSetFactory factory) { - try (TimedOperation to = TimeTracker.startOperation(TimedOperationCategory.LOAD_RULES)) { - return getRuleSets(rulesets, factory); - } - } - - /** - * @deprecated Use a {@link RuleSetLoader} - */ - @InternalApi - @Deprecated - public static RuleSetFactory getRulesetFactory(final PMDConfiguration configuration, - final ResourceLoader resourceLoader) { - return new RuleSetFactory(resourceLoader, configuration.getMinimumPriority(), true, - configuration.isRuleSetFactoryCompatibilityEnabled()); - } - - /** - * Returns a ruleset factory which uses the classloader for PMD - * classes to resolve resource references. - * - * @param configuration PMD configuration, contains info about the - * factory parameters - * - * @return A ruleset factory - * - * @see #createFactory(PMDConfiguration, ClassLoader) - * - * @deprecated Use {@link RuleSetLoader#fromPmdConfig(PMDConfiguration)} - */ - @Deprecated - public static RuleSetFactory createFactory(final PMDConfiguration configuration) { - return createFactory(configuration, RulesetsFactoryUtils.class.getClassLoader()); - } - - /** - * Returns a ruleset factory with default parameters. It doesn't prune - * rules based on priority, and doesn't warn for deprecations. - * - * @return A ruleset factory - * - * @see RuleSetLoader - */ - public static RuleSetFactory defaultFactory() { - return new RuleSetFactory(); - } - - /** - * Returns a ruleset factory which uses the provided {@link ClassLoader} - * to resolve resource references. It warns for deprecated rule usages. - * - * @param configuration PMD configuration, contains info about the - * factory parameters - * @param classLoader Class loader to load resources - * - * @return A ruleset factory - * - * @see #createFactory(PMDConfiguration) - * - * @deprecated Use a {@link RuleSetLoader} - */ - @Deprecated - public static RuleSetFactory createFactory(final PMDConfiguration configuration, ClassLoader classLoader) { - return createFactory(classLoader, - configuration.getMinimumPriority(), - true, - configuration.isRuleSetFactoryCompatibilityEnabled()); - } - - /** - * Returns a ruleset factory which uses the provided {@link ClassLoader} - * to resolve resource references. - * - * @param minimumPriority Minimum priority for rules to be included - * @param warnDeprecated If true, print warnings when deprecated rules are included - * @param enableCompatibility If true, rule references to moved rules are mapped to their - * new location if they are known - * @param classLoader Class loader to load resources - * - * @return A ruleset factory - * - * @see #createFactory(PMDConfiguration) - * - * @deprecated Use a {@link RuleSetLoader} - */ - @Deprecated - public static RuleSetFactory createFactory(ClassLoader classLoader, - RulePriority minimumPriority, - boolean warnDeprecated, - boolean enableCompatibility) { - - return new RuleSetFactory(new ResourceLoader(classLoader), minimumPriority, warnDeprecated, enableCompatibility); - } - - /** - * Returns a ruleset factory which uses the classloader for PMD - * classes to resolve resource references. - * - * @param minimumPriority Minimum priority for rules to be included - * @param warnDeprecated If true, print warnings when deprecated rules are included - * @param enableCompatibility If true, rule references to moved rules are mapped to their - * new location if they are known - * - * @return A ruleset factory - * - * @see #createFactory(PMDConfiguration) - * - * @deprecated Use a {@link RuleSetLoader} - */ - @Deprecated - public static RuleSetFactory createFactory(RulePriority minimumPriority, - boolean warnDeprecated, - boolean enableCompatibility) { - return new RuleSetFactory(new ResourceLoader(), minimumPriority, warnDeprecated, enableCompatibility); - } - - /** - * Returns a ruleset factory which uses the classloader for PMD - * classes to resolve resource references. - * - * @param minimumPriority Minimum priority for rules to be included - * @param warnDeprecated If true, print warnings when deprecated rules are included - * @param enableCompatibility If true, rule references to moved rules are mapped to their - * new location if they are known - * @param includeDeprecatedRuleReferences If true, deprecated rule references are retained. Usually, these - * references are ignored, since they indicate renamed/moved rules, and the referenced - * rule is often included in the same ruleset. Enabling this might result in - * duplicated rules. - * - * @return A ruleset factory - * - * @see #createFactory(PMDConfiguration) - * @deprecated Use a {@link RuleSetLoader} - */ - @Deprecated - public static RuleSetFactory createFactory(RulePriority minimumPriority, - boolean warnDeprecated, - boolean enableCompatibility, - boolean includeDeprecatedRuleReferences) { - - return new RuleSetFactory(new ResourceLoader(), minimumPriority, warnDeprecated, enableCompatibility, - includeDeprecatedRuleReferences); - } - - /** - * If in debug modus, print the names of the rules. - * - * @param rulesets the RuleSets to print - */ - private static void printRuleNamesInDebug(RuleSets rulesets) { - if (LOG.isLoggable(Level.FINER)) { - for (Rule r : rulesets.getAllRules()) { - LOG.finer("Loaded rule " + r.getName()); - } - } - } -} diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java b/pmd-core/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java index c59ccd52e1..9fba58463c 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/ant/internal/PMDTaskImpl.java @@ -29,8 +29,6 @@ import net.sourceforge.pmd.Rule; import net.sourceforge.pmd.RulePriority; import net.sourceforge.pmd.RuleSet; import net.sourceforge.pmd.RuleSetLoader; -import net.sourceforge.pmd.RuleSets; -import net.sourceforge.pmd.RulesetsFactoryUtils; import net.sourceforge.pmd.ant.Formatter; import net.sourceforge.pmd.ant.PMDTask; import net.sourceforge.pmd.ant.SourceLanguage; @@ -53,6 +51,7 @@ public class PMDTaskImpl { private final List formatters = new ArrayList<>(); private final List filesets = new ArrayList<>(); private final PMDConfiguration configuration = new PMDConfiguration(); + private final String rulesetPaths; private boolean failOnError; private boolean failOnRuleViolation; private int maxRuleViolations = 0; @@ -70,7 +69,7 @@ public class PMDTaskImpl { if (this.maxRuleViolations > 0) { this.failOnRuleViolation = true; } - configuration.setRuleSets(task.getRulesetFiles()); + this.rulesetPaths = task.getRulesetFiles() == null ? "" : task.getRulesetFiles(); configuration.setRuleSetFactoryCompatibilityEnabled(!task.isNoRuleSetCompatibility()); if (task.getEncoding() != null) { configuration.setSourceEncoding(task.getEncoding()); @@ -108,15 +107,15 @@ public class PMDTaskImpl { .loadResourcesWith(setupResourceLoader()); // This is just used to validate and display rules. Each thread will create its own ruleset - String ruleSetString = configuration.getRuleSets(); - if (StringUtils.isNotBlank(ruleSetString)) { - // Substitute env variables/properties - configuration.setRuleSets(project.replaceProperties(ruleSetString)); - } + // Substitute env variables/properties + String ruleSetString = project.replaceProperties(rulesetPaths); - final RuleSets ruleSets = RulesetsFactoryUtils.getRuleSets(configuration.getRuleSets(), rulesetLoader.toFactory()); - List rulesetList = Arrays.asList(ruleSets.getAllRuleSets()); - logRulesUsed(ruleSets); + List rulesets = Arrays.asList(ruleSetString.split(",")); + List rulesetList = rulesetLoader.loadFromResources(rulesets); + if (rulesetList.isEmpty()) { + throw new BuildException("No rulesets"); + } + logRulesUsed(rulesetList); if (configuration.getSuppressMarker() != null) { project.log("Setting suppress marker to be " + configuration.getSuppressMarker(), Project.MSG_VERBOSE); @@ -294,10 +293,10 @@ public class PMDTaskImpl { } } - private void logRulesUsed(RuleSets rules) { - project.log("Using these rulesets: " + configuration.getRuleSets(), Project.MSG_VERBOSE); + private void logRulesUsed(List rulesets) { + project.log("Using these rulesets: " + rulesetPaths, Project.MSG_VERBOSE); - for (RuleSet ruleSet : rules.getAllRuleSets()) { + for (RuleSet ruleSet : rulesets) { for (Rule rule : ruleSet.getRules()) { project.log("Using rule " + rule.getName(), Project.MSG_VERBOSE); } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java b/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java index dc0910bca1..c8e4cd7941 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDParameters.java @@ -6,6 +6,7 @@ package net.sourceforge.pmd.cli; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Properties; @@ -203,7 +204,7 @@ public class PMDParameters { configuration.setReportFile(this.getReportfile()); configuration.setReportProperties(this.getProperties()); configuration.setReportShortNames(this.isShortnames()); - configuration.setRuleSets(this.getRulesets()); + configuration.setRuleSets(Arrays.asList(this.getRulesets().split(","))); configuration.setRuleSetFactoryCompatibilityEnabled(!this.noRuleSetCompatibility); configuration.setShowSuppressedViolations(this.isShowsuppressed()); configuration.setSourceEncoding(this.getEncoding()); diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ParserOptions.java b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ParserOptions.java index 0834306906..b92d9c7b67 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/lang/ParserOptions.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/lang/ParserOptions.java @@ -4,11 +4,14 @@ package net.sourceforge.pmd.lang; +import java.util.Locale; import java.util.Objects; import org.checkerframework.checker.nullness.qual.NonNull; import net.sourceforge.pmd.PMD; +import net.sourceforge.pmd.properties.AbstractPropertySource; +import net.sourceforge.pmd.properties.PropertyDescriptor; import net.sourceforge.pmd.lang.ast.Parser; /** @@ -20,6 +23,23 @@ import net.sourceforge.pmd.lang.ast.Parser; public class ParserOptions { private String suppressMarker = PMD.SUPPRESS_MARKER; + /** + * Language used to construct environment variable names that match PropertyDescriptors. + */ + private final String languageId; + + private final ParserOptionsProperties parserOptionsProperties; + + public ParserOptions() { + this.languageId = null; + this.parserOptionsProperties = new ParserOptionsProperties(); + } + + public ParserOptions(String languageId) { + this.languageId = Objects.requireNonNull(languageId); + this.parserOptionsProperties = new ParserOptionsProperties(); + } + public final @NonNull String getSuppressMarker() { return suppressMarker; } @@ -29,12 +49,21 @@ public class ParserOptions { this.suppressMarker = suppressMarker; } - public ParserOptions() { - this(PMD.SUPPRESS_MARKER); + protected final void defineProperty(PropertyDescriptor propertyDescriptor) { + parserOptionsProperties.definePropertyDescriptor(propertyDescriptor); } - public ParserOptions(String suppressMarker) { - setSuppressMarker(suppressMarker); + protected final void defineProperty(PropertyDescriptor propertyDescriptor, T initialValue) { + defineProperty(propertyDescriptor); + setProperty(propertyDescriptor, initialValue); + } + + protected final void setProperty(PropertyDescriptor propertyDescriptor, T initialValue) { + parserOptionsProperties.setProperty(propertyDescriptor, initialValue); + } + + public final T getProperty(PropertyDescriptor propertyDescriptor) { + return parserOptionsProperties.getProperty(propertyDescriptor); } @Override @@ -46,11 +75,84 @@ public class ParserOptions { return false; } final ParserOptions that = (ParserOptions) obj; - return this.getSuppressMarker().equals(that.getSuppressMarker()); + return Objects.equals(suppressMarker, that.suppressMarker) + && Objects.equals(languageId, that.languageId) + && Objects.equals(parserOptionsProperties, that.parserOptionsProperties); } @Override public int hashCode() { - return getSuppressMarker().hashCode(); + return Objects.hash(suppressMarker, languageId, parserOptionsProperties); + } + + /** + * Returns the environment variable name that a user can set in order to override the default value. + */ + String getEnvironmentVariableName(PropertyDescriptor propertyDescriptor) { + if (languageId == null) { + throw new IllegalStateException("Language is null"); + } + return "PMD_" + languageId.toUpperCase(Locale.ROOT) + "_" + + propertyDescriptor.name().toUpperCase(Locale.ROOT); + } + + /** + * @return environment variable that overrides the PropertyDesciptors default value. Returns null if no environment + * variable has been set. + */ + String getEnvValue(PropertyDescriptor propertyDescriptor) { + // note: since we use environent variables and not system properties, + // tests override this method. + return System.getenv(getEnvironmentVariableName(propertyDescriptor)); + } + + /** + * Overrides the default PropertyDescriptors with values found in environment variables. + * TODO: Move this to net.sourceforge.pmd.PMD#parserFor when CLI options are implemented + */ + protected final void overridePropertiesFromEnv() { + for (PropertyDescriptor propertyDescriptor : parserOptionsProperties.getPropertyDescriptors()) { + String propertyValue = getEnvValue(propertyDescriptor); + + if (propertyValue != null) { + setPropertyCapture(propertyDescriptor, propertyValue); + } + } + } + + private void setPropertyCapture(PropertyDescriptor propertyDescriptor, String propertyValue) { + T value = propertyDescriptor.valueFrom(propertyValue); + parserOptionsProperties.setProperty(propertyDescriptor, value); + } + + private final class ParserOptionsProperties extends AbstractPropertySource { + + @Override + protected String getPropertySourceType() { + return "ParserOptions"; + } + + @Override + public String getName() { + return ParserOptions.this.getClass().getSimpleName(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ParserOptionsProperties)) { + return false; + } + final ParserOptionsProperties that = (ParserOptionsProperties) obj; + return Objects.equals(getPropertiesByPropertyDescriptor(), + that.getPropertiesByPropertyDescriptor()); + } + + @Override + public int hashCode() { + return getPropertiesByPropertyDescriptor().hashCode(); + } } } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/processor/AbstractPMDProcessor.java b/pmd-core/src/main/java/net/sourceforge/pmd/processor/AbstractPMDProcessor.java index dd5b9081fb..7869210e92 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/processor/AbstractPMDProcessor.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/processor/AbstractPMDProcessor.java @@ -18,9 +18,7 @@ import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.Report; import net.sourceforge.pmd.Rule; import net.sourceforge.pmd.RuleContext; -import net.sourceforge.pmd.RuleSetFactory; import net.sourceforge.pmd.RuleSets; -import net.sourceforge.pmd.RulesetsFactoryUtils; import net.sourceforge.pmd.SourceCodeProcessor; import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.benchmark.TimeTracker; @@ -68,22 +66,6 @@ public abstract class AbstractPMDProcessor { return dataSource.getNiceFileName(configuration.isReportShortNames(), configuration.getInputPaths()); } - /** - * Create instances for each rule defined in the ruleset(s) in the - * configuration. Please note, that the returned instances must - * not be used by different threads. Each thread must create its - * own copy of the rules. - * - * @param factory The factory used to create the configured rule sets - * @param report The base report on which to report any configuration errors - * @return the rules within a rulesets - */ - protected RuleSets createRuleSets(RuleSetFactory factory, Report report) { - final RuleSets rs = RulesetsFactoryUtils.getRuleSets(configuration.getRuleSets(), factory); - reportBrokenRules(report, rs); - return rs; - } - public static void reportBrokenRules(Report report, RuleSets rs) { final Set brokenRules = removeBrokenRules(rs); for (final Rule rule : brokenRules) { @@ -112,16 +94,6 @@ public abstract class AbstractPMDProcessor { return brokenRules; } - @SuppressWarnings("PMD.CloseResource") - // the data sources must only be closed after the threads are finished - // this is done manually without a try-with-resources - @Deprecated - public void processFiles(RuleSetFactory ruleSetFactory, List files, RuleContext ctx, - List renderers) { - RuleSets rs = createRuleSets(ruleSetFactory, ctx.getReport()); - processFiles(rs, files, ctx, renderers); - } - @SuppressWarnings("PMD.CloseResource") // the data sources must only be closed after the threads are finished // this is done manually without a try-with-resources diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/properties/AbstractPropertySource.java b/pmd-core/src/main/java/net/sourceforge/pmd/properties/AbstractPropertySource.java index 3d7d7a3c95..2cb4beb2a3 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/properties/AbstractPropertySource.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/properties/AbstractPropertySource.java @@ -216,4 +216,5 @@ public abstract class AbstractPropertySource implements PropertySource { private String errorForPropCapture(PropertyDescriptor descriptor) { return descriptor.errorFor(getProperty(descriptor)); } + } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/properties/PropertyDescriptorField.java b/pmd-core/src/main/java/net/sourceforge/pmd/properties/PropertyDescriptorField.java index 8a01f07572..854dab11d5 100755 --- a/pmd-core/src/main/java/net/sourceforge/pmd/properties/PropertyDescriptorField.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/properties/PropertyDescriptorField.java @@ -6,15 +6,12 @@ package net.sourceforge.pmd.properties; import java.util.Objects; -import net.sourceforge.pmd.RuleSetFactory; - /** * Field names for parsing the properties out of the ruleset xml files. These are intended to be used as the keys to a * map of fields to values. Most property descriptors can be built directly from such a map using their factory. * * @author Brian Remedios - * @see RuleSetFactory * @see PropertyTypeId * @deprecated Will be removed with 7.0.0 */ diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/ResourceLoader.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/ResourceLoader.java index 5f9fe917bc..d9d4d7e5b0 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/util/ResourceLoader.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/ResourceLoader.java @@ -5,6 +5,7 @@ package net.sourceforge.pmd.util; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; @@ -13,8 +14,10 @@ import java.net.URLConnection; import java.nio.file.Files; import java.util.Objects; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + import net.sourceforge.pmd.Rule; -import net.sourceforge.pmd.RuleSetNotFoundException; import net.sourceforge.pmd.annotation.InternalApi; /** @@ -58,10 +61,10 @@ public class ResourceLoader { * Caller is responsible for closing the {@link InputStream}. * * @param name The resource to attempt and load + * * @return InputStream - * @throws RuleSetNotFoundException */ - public InputStream loadResourceAsStream(final String name) throws RuleSetNotFoundException { + public @NonNull InputStream loadResourceAsStream(final String name) throws IOException { // Search file locations first final File file = new File(name); if (file.exists()) { @@ -69,7 +72,8 @@ public class ResourceLoader { return Files.newInputStream(file.toPath()); } catch (final IOException e) { // if the file didn't exist, we wouldn't be here - throw new RuntimeException(e); // somehow the file vanished between checking for existence and opening + // somehow the file vanished between checking for existence and opening + throw new IOException("File was checked to exist", e); } } @@ -78,20 +82,18 @@ public class ResourceLoader { final HttpURLConnection connection = (HttpURLConnection) new URL(name).openConnection(); connection.setConnectTimeout(TIMEOUT); connection.setReadTimeout(TIMEOUT); - return connection.getInputStream(); - } catch (final Exception e) { - try { - return loadClassPathResourceAsStream(name); - } catch (final IOException ignored) { - // We will throw our own exception, with a different message + InputStream is = connection.getInputStream(); + if (is != null) { + return is; } + } catch (final Exception e) { + return loadClassPathResourceAsStreamOrThrow(name); } - throw new RuleSetNotFoundException("Can't find resource " + name - + ". Make sure the resource is a valid file or URL or is on the CLASSPATH"); + throw new IOException("Can't find resource " + name + ". Make sure the resource is a valid file or URL or is on the classpath"); } - public InputStream loadClassPathResourceAsStream(final String name) throws IOException { + public @Nullable InputStream loadClassPathResourceAsStream(final String name) throws IOException { /* * Don't use getResourceAsStream to avoid reusing connections between threads * See https://github.com/pmd/pmd/issues/234 @@ -110,7 +112,7 @@ public class ResourceLoader { } } - public InputStream loadClassPathResourceAsStreamOrThrow(final String name) throws RuleSetNotFoundException { + public @NonNull InputStream loadClassPathResourceAsStreamOrThrow(final String name) throws IOException { InputStream is = null; try { is = loadClassPathResourceAsStream(name); @@ -119,8 +121,8 @@ public class ResourceLoader { } if (is == null) { - throw new RuleSetNotFoundException("Can't find resource " + name - + ". Make sure the resource is on the CLASSPATH"); + throw new FileNotFoundException("Can't find resource " + name + + ". Make sure the resource is on the classpath"); } return is; @@ -128,12 +130,6 @@ public class ResourceLoader { /** * Load the rule from the classloader from resource loader, consistent with the ruleset - * - * @param clazz - * @return - * @throws ClassNotFoundException - * @throws IllegalAccessException - * @throws InstantiationException */ public Rule loadRuleFromClassPath(final String clazz) throws ClassNotFoundException, IllegalAccessException, InstantiationException { return (Rule) classLoader.loadClass(clazz).newInstance(); diff --git a/pmd-core/src/main/resources/rulesets/releases/33.xml b/pmd-core/src/main/resources/rulesets/releases/33.xml index 08d1dcd8bf..895e7820d9 100644 --- a/pmd-core/src/main/resources/rulesets/releases/33.xml +++ b/pmd-core/src/main/resources/rulesets/releases/33.xml @@ -8,7 +8,7 @@ This ruleset contains links to rules that are new in PMD v3.3 - + diff --git a/pmd-core/src/main/resources/rulesets/releases/510.xml b/pmd-core/src/main/resources/rulesets/releases/510.xml index 82e6c2530e..09a1172297 100644 --- a/pmd-core/src/main/resources/rulesets/releases/510.xml +++ b/pmd-core/src/main/resources/rulesets/releases/510.xml @@ -12,7 +12,7 @@ This ruleset contains links to rules that are new in PMD v5.1.0 - + diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/ConfigurationTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/ConfigurationTest.java index 480bdb24a1..d765920004 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/ConfigurationTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/ConfigurationTest.java @@ -111,13 +111,6 @@ public class ConfigurationTest { Assert.assertArrayEquals(expectedUris, uris); } - @Test - public void testRuleSets() { - PMDConfiguration configuration = new PMDConfiguration(); - assertEquals("Default RuleSets", null, configuration.getRuleSets()); - configuration.setRuleSets("/rulesets/basic.xml"); - assertEquals("Changed RuleSets", "/rulesets/basic.xml", configuration.getRuleSets()); - } @Test public void testMinimumPriority() { diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryCompatibilityTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryCompatibilityTest.java index 7dfb6e1f09..bbc4a5db96 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryCompatibilityTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryCompatibilityTest.java @@ -4,17 +4,9 @@ package net.sourceforge.pmd; -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.Reader; -import java.nio.charset.StandardCharsets; - -import org.apache.commons.io.IOUtils; import org.junit.Assert; import org.junit.Test; -import net.sourceforge.pmd.util.ResourceLoader; - public class RuleSetFactoryCompatibilityTest { @Test @@ -26,37 +18,30 @@ public class RuleSetFactoryCompatibilityTest { + " Test\n" + "\n" + " \n" + "\n"; - RuleSetFactory factory = RulesetsFactoryUtils.defaultFactory(); - factory.getCompatibilityFilter().addFilterRuleMoved("dummy", "notexisting", "basic", "DummyBasicMockRule"); + RuleSetFactoryCompatibility compat = new RuleSetFactoryCompatibility(); + compat.addFilterRuleMoved("dummy", "notexisting", "basic", "DummyBasicMockRule"); + + + RuleSetLoader factory = new RuleSetLoader().setCompatibility(compat); + RuleSet createdRuleSet = factory.loadFromString("dummy.xml", ruleset); - RuleSet createdRuleSet = createRulesetFromString(ruleset, factory); Assert.assertNotNull(createdRuleSet.getRuleByName("DummyBasicMockRule")); } @Test - public void testCorrectMovedAndRename() throws Exception { - final String ruleset = "\n" + "\n" + "\n" - + " Test\n" + "\n" - + " \n" + "\n"; + public void testCorrectMovedAndRename() { RuleSetFactoryCompatibility rsfc = new RuleSetFactoryCompatibility(); rsfc.addFilterRuleMoved("dummy", "notexisting", "basic", "OldDummyBasicMockRule"); rsfc.addFilterRuleRenamed("dummy", "basic", "OldDummyBasicMockRule", "NewNameForDummyBasicMockRule"); - InputStream stream = new ByteArrayInputStream(ruleset.getBytes(StandardCharsets.ISO_8859_1)); - Reader filtered = rsfc.filterRuleSetFile(stream); - String out = IOUtils.toString(filtered); + String out = rsfc.applyRef("rulesets/dummy/notexisting.xml/OldDummyBasicMockRule"); - Assert.assertFalse(out.contains("notexisting.xml")); - Assert.assertFalse(out.contains("OldDummyBasicMockRule")); - Assert.assertTrue(out.contains("")); + Assert.assertEquals("rulesets/dummy/basic.xml/NewNameForDummyBasicMockRule", out); } @Test - public void testExclusion() throws Exception { + public void testExclusion() { final String ruleset = "\n" + "\n" + "Test\n" + "\n" + " \n" + " \n" + " \n" + "\n"; - RuleSetFactory factory = RulesetsFactoryUtils.defaultFactory(); - factory.getCompatibilityFilter().addFilterRuleRenamed("dummy", "basic", "OldNameOfSampleXPathRule", - "SampleXPathRule"); + RuleSetFactoryCompatibility compat = new RuleSetFactoryCompatibility(); + compat.addFilterRuleRenamed("dummy", "basic", "OldNameOfSampleXPathRule", "SampleXPathRule"); + + RuleSetLoader factory = new RuleSetLoader().setCompatibility(compat); + RuleSet createdRuleSet = factory.loadFromString("dummy.xml", ruleset); - RuleSet createdRuleSet = createRulesetFromString(ruleset, factory); Assert.assertNotNull(createdRuleSet.getRuleByName("DummyBasicMockRule")); Assert.assertNull(createdRuleSet.getRuleByName("SampleXPathRule")); } @Test - public void testExclusionRenamedAndMoved() throws Exception { - final String ruleset = "\n" + "\n" + "\n" - + " Test\n" + "\n" - + " \n" - + " \n" - + " \n" - + "\n"; + public void testExclusionRenamedAndMoved() { RuleSetFactoryCompatibility rsfc = new RuleSetFactoryCompatibility(); rsfc.addFilterRuleMovedAndRenamed("dummy", "oldbasic", "OldDummyBasicMockRule", "basic", "NewNameForDummyBasicMockRule"); - InputStream stream = new ByteArrayInputStream(ruleset.getBytes(StandardCharsets.ISO_8859_1)); - Reader filtered = rsfc.filterRuleSetFile(stream); - String out = IOUtils.toString(filtered); + String in = "rulesets/dummy/oldbasic.xml"; + String out = rsfc.applyRef(in); - Assert.assertTrue(out.contains("OldDummyBasicMockRule")); + Assert.assertEquals(in, out); } @Test - public void testFilter() throws Exception { + public void testFilter() { RuleSetFactoryCompatibility rsfc = new RuleSetFactoryCompatibility(); rsfc.addFilterRuleMoved("dummy", "notexisting", "basic", "DummyBasicMockRule"); rsfc.addFilterRuleRemoved("dummy", "basic", "DeletedRule"); rsfc.addFilterRuleRenamed("dummy", "basic", "OldNameOfBasicMockRule", "NewNameOfBasicMockRule"); - String in = "\n" + "\n" + "\n" - + " Test\n" + "\n" - + " \n" - + " \n" - + " \n" + "\n"; - InputStream stream = new ByteArrayInputStream(in.getBytes(StandardCharsets.ISO_8859_1)); - Reader filtered = rsfc.filterRuleSetFile(stream); - String out = IOUtils.toString(filtered); + Assert.assertEquals("rulesets/dummy/basic.xml/DummyBasicMockRule", + rsfc.applyRef("rulesets/dummy/notexisting.xml/DummyBasicMockRule")); - Assert.assertFalse(out.contains("notexisting.xml")); - Assert.assertTrue(out.contains("")); + Assert.assertEquals("rulesets/dummy/basic.xml/NewNameOfBasicMockRule", + rsfc.applyRef("rulesets/dummy/basic.xml/OldNameOfBasicMockRule")); - Assert.assertFalse(out.contains("DeletedRule")); - - Assert.assertFalse(out.contains("OldNameOfBasicMockRule")); - Assert.assertTrue(out.contains("")); + Assert.assertNull(rsfc.applyRef("rulesets/dummy/basic.xml/DeletedRule")); } @Test - public void testExclusionFilter() throws Exception { + public void testExclusionFilter() { RuleSetFactoryCompatibility rsfc = new RuleSetFactoryCompatibility(); rsfc.addFilterRuleRenamed("dummy", "basic", "AnotherOldNameOfBasicMockRule", "NewNameOfBasicMockRule"); - String in = "\n" + "\n" + "\n" - + " Test\n" + "\n" + " \n" - + " \n" + " \n" + "\n"; - InputStream stream = new ByteArrayInputStream(in.getBytes(StandardCharsets.ISO_8859_1)); - Reader filtered = rsfc.filterRuleSetFile(stream); - String out = IOUtils.toString(filtered); + String out = rsfc.applyExclude("rulesets/dummy/basic.xml", "AnotherOldNameOfBasicMockRule", false); - Assert.assertFalse(out.contains("OldNameOfBasicMockRule")); - Assert.assertTrue(out.contains("")); + Assert.assertEquals("NewNameOfBasicMockRule", out); } - @Test - public void testEncoding() { - RuleSetFactoryCompatibility rsfc = new RuleSetFactoryCompatibility(); - String testString; - - testString = ""; - Assert.assertEquals("ISO-8859-1", rsfc.determineEncoding(testString.getBytes(StandardCharsets.ISO_8859_1))); - - testString = ""; - Assert.assertEquals("UTF-8", rsfc.determineEncoding(testString.getBytes(StandardCharsets.ISO_8859_1))); - } - - private RuleSet createRulesetFromString(final String ruleset, RuleSetFactory factory) - throws RuleSetNotFoundException { - return factory.createRuleSet(new RuleSetReferenceId(null) { - @Override - public InputStream getInputStream(ResourceLoader resourceLoader) throws RuleSetNotFoundException { - return new ByteArrayInputStream(ruleset.getBytes(StandardCharsets.UTF_8)); - } - }); - } } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryDuplicatedRuleLoggingTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryDuplicatedRuleLoggingTest.java index 38f58273c5..64bb25fce4 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryDuplicatedRuleLoggingTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryDuplicatedRuleLoggingTest.java @@ -22,10 +22,10 @@ public class RuleSetFactoryDuplicatedRuleLoggingTest { public LocaleRule localeRule = LocaleRule.en(); @org.junit.Rule - public JavaUtilLoggingRule logging = new JavaUtilLoggingRule(RuleSetFactory.class.getName()); + public JavaUtilLoggingRule logging = new JavaUtilLoggingRule(RuleSetLoader.class.getName()); @Test - public void duplicatedRuleReferenceShouldWarn() throws RuleSetNotFoundException { + public void duplicatedRuleReferenceShouldWarn() { RuleSet ruleset = loadRuleSet("duplicatedRuleReference.xml"); assertEquals(1, ruleset.getRules().size()); @@ -37,7 +37,7 @@ public class RuleSetFactoryDuplicatedRuleLoggingTest { } @Test - public void duplicatedRuleReferenceWithOverrideShouldNotWarn() throws RuleSetNotFoundException { + public void duplicatedRuleReferenceWithOverrideShouldNotWarn() { RuleSet ruleset = loadRuleSet("duplicatedRuleReferenceWithOverride.xml"); assertEquals(2, ruleset.getRules().size()); @@ -49,7 +49,7 @@ public class RuleSetFactoryDuplicatedRuleLoggingTest { } @Test - public void duplicatedRuleReferenceWithOverrideBeforeShouldNotWarn() throws RuleSetNotFoundException { + public void duplicatedRuleReferenceWithOverrideBeforeShouldNotWarn() { RuleSet ruleset = loadRuleSet("duplicatedRuleReferenceWithOverrideBefore.xml"); assertEquals(2, ruleset.getRules().size()); @@ -61,7 +61,7 @@ public class RuleSetFactoryDuplicatedRuleLoggingTest { } @Test - public void multipleDuplicates() throws RuleSetNotFoundException { + public void multipleDuplicates() { RuleSet ruleset = loadRuleSet("multipleDuplicates.xml"); assertEquals(2, ruleset.getRules().size()); @@ -74,8 +74,7 @@ public class RuleSetFactoryDuplicatedRuleLoggingTest { assertTrue(logging.getLog().contains("The ruleset rulesets/dummy/basic.xml is referenced multiple times in \"Custom Rules\".")); } - private RuleSet loadRuleSet(String ruleSetFilename) throws RuleSetNotFoundException { - RuleSetFactory rsf = RulesetsFactoryUtils.defaultFactory(); - return rsf.createRuleSet("net/sourceforge/pmd/rulesets/duplicatedRuleLoggingTest/" + ruleSetFilename); + private RuleSet loadRuleSet(String ruleSetFilename) { + return new RuleSetLoader().loadFromResource("net/sourceforge/pmd/rulesets/duplicatedRuleLoggingTest/" + ruleSetFilename); } } 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 52e52f4ff5..0b0bab592b 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java @@ -4,23 +4,23 @@ package net.sourceforge.pmd; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashSet; -import java.util.List; import java.util.Set; import org.apache.commons.lang3.StringUtils; -import org.hamcrest.Matchers; +import org.junit.Assert; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -41,26 +41,21 @@ public class RuleSetFactoryTest { @org.junit.Rule public LocaleRule localeRule = LocaleRule.en(); - @Test - public void testRuleSetFileName() throws RuleSetNotFoundException { - RuleSet rs = loadRuleSet(EMPTY_RULESET); - assertNull("RuleSet file name not expected", rs.getFileName()); + @org.junit.Rule + public JavaUtilLoggingRule logging = new JavaUtilLoggingRule(RuleSetLoader.class.getName()); - RuleSetFactory rsf = RulesetsFactoryUtils.defaultFactory(); - rs = rsf.createRuleSet("net/sourceforge/pmd/TestRuleset1.xml"); + @Test + public void testRuleSetFileName() { + RuleSet rs = new RuleSetLoader().loadFromString("dummyRuleset.xml", EMPTY_RULESET); + assertEquals("dummyRuleset.xml", rs.getFileName()); + + rs = new RuleSetLoader().loadFromResource("net/sourceforge/pmd/TestRuleset1.xml"); assertEquals("wrong RuleSet file name", rs.getFileName(), "net/sourceforge/pmd/TestRuleset1.xml"); } @Test - public void testNoRuleSetFileName() throws RuleSetNotFoundException { - RuleSet rs = loadRuleSet(EMPTY_RULESET); - assertNull("RuleSet file name not expected", rs.getFileName()); - } - - @Test - public void testRefs() throws Exception { - RuleSetFactory rsf = RulesetsFactoryUtils.defaultFactory(); - RuleSet rs = rsf.createRuleSet("net/sourceforge/pmd/TestRuleset1.xml"); + public void testRefs() { + RuleSet rs = new RuleSetLoader().loadFromResource("net/sourceforge/pmd/TestRuleset1.xml"); assertNotNull(rs.getRuleByName("TestRuleRef")); } @@ -70,8 +65,7 @@ public class RuleSetFactoryTest { assertNotNull("Test ruleset not found - can't continue with test!", in); in.close(); - RuleSetFactory rsf = RulesetsFactoryUtils.defaultFactory(); - RuleSets rs = rsf.createRuleSets("net/sourceforge/pmd/rulesets/reference-ruleset.xml"); + RuleSet rs = new RuleSetLoader().loadFromResource("net/sourceforge/pmd/rulesets/reference-ruleset.xml"); // added by referencing a complete ruleset (TestRuleset1.xml) assertNotNull(rs.getRuleByName("MockRule1")); assertNotNull(rs.getRuleByName("MockRule2")); @@ -92,7 +86,7 @@ public class RuleSetFactoryTest { // assert that MockRule2 is only once added to the ruleset, so that it // really // overwrites the configuration inherited from TestRuleset1.xml - assertEquals(1, countRule(rs, "MockRule2")); + assertNotNull(rs.getRuleByName("MockRule2")); Rule mockRule1 = rs.getRuleByName("MockRule1"); assertNotNull(mockRule1); @@ -109,38 +103,27 @@ public class RuleSetFactoryTest { Rule ruleset4Rule1 = rs.getRuleByName("Ruleset4Rule1"); assertNotNull(ruleset4Rule1); assertEquals(5, ruleset4Rule1.getPriority().getPriority()); - assertEquals(1, countRule(rs, "Ruleset4Rule1")); + assertNotNull(rs.getRuleByName("Ruleset4Rule1")); // priority overridden for whole TestRuleset4 group Rule ruleset4Rule2 = rs.getRuleByName("Ruleset4Rule2"); assertNotNull(ruleset4Rule2); assertEquals(2, ruleset4Rule2.getPriority().getPriority()); } - private int countRule(RuleSets rs, String ruleName) { - int count = 0; - for (Rule r : rs.getAllRules()) { - if (ruleName.equals(r.getName())) { - count++; - } - } - return count; - } - - @Test(expected = RuleSetNotFoundException.class) - public void testRuleSetNotFound() throws RuleSetNotFoundException { - RuleSetFactory rsf = RulesetsFactoryUtils.defaultFactory(); - rsf.createRuleSet("fooooo"); + @Test + public void testRuleSetNotFound() { + assertThrows(RuleSetLoadException.class, () -> new RuleSetLoader().loadFromResource("fooooo")); } @Test - public void testCreateEmptyRuleSet() throws RuleSetNotFoundException { + public void testCreateEmptyRuleSet() { RuleSet rs = loadRuleSet(EMPTY_RULESET); assertEquals("test", rs.getName()); assertEquals(0, rs.size()); } @Test - public void testSingleRule() throws RuleSetNotFoundException { + public void testSingleRule() { RuleSet rs = loadRuleSet(SINGLE_RULE); assertEquals(1, rs.size()); Rule r = rs.getRules().iterator().next(); @@ -150,7 +133,7 @@ public class RuleSetFactoryTest { } @Test - public void testMultipleRules() throws RuleSetNotFoundException { + public void testMultipleRules() { RuleSet rs = loadRuleSet(MULTIPLE_RULES); assertEquals(2, rs.size()); Set expected = new HashSet<>(); @@ -162,58 +145,59 @@ public class RuleSetFactoryTest { } @Test - public void testSingleRuleWithPriority() throws RuleSetNotFoundException { + public void testSingleRuleWithPriority() { assertEquals(RulePriority.MEDIUM, loadFirstRule(PRIORITY).getPriority()); } @Test - @SuppressWarnings("unchecked") - public void testProps() throws RuleSetNotFoundException { + public void testProps() { Rule r = loadFirstRule(PROPERTIES); - assertEquals("bar", r.getProperty((PropertyDescriptor) r.getPropertyDescriptor("fooString"))); - assertEquals(new Integer(3), r.getProperty((PropertyDescriptor) r.getPropertyDescriptor("fooInt"))); - assertTrue(r.getProperty((PropertyDescriptor) r.getPropertyDescriptor("fooBoolean"))); - assertEquals(3.0d, r.getProperty((PropertyDescriptor) r.getPropertyDescriptor("fooDouble")), 0.05); + assertEquals("bar", r.getProperty(r.getPropertyDescriptor("fooString"))); + assertEquals(3, r.getProperty(r.getPropertyDescriptor("fooInt"))); + assertEquals(true, r.getProperty(r.getPropertyDescriptor("fooBoolean"))); + assertEquals(3.0d, (Double) r.getProperty(r.getPropertyDescriptor("fooDouble")), 0.05); assertNull(r.getPropertyDescriptor("BuggleFish")); assertNotSame(r.getDescription().indexOf("testdesc2"), -1); } @Test - public void testStringMultiPropertyDefaultDelimiter() throws Exception { - Rule r = loadFirstRule("\n\n Desc\n" - + " \n" - + " Please move your class to the right folder(rest \nfolder)\n" - + " 2\n \n \n "); - PropertyDescriptor> prop = (PropertyDescriptor>) r.getPropertyDescriptor("packageRegEx"); - List values = r.getProperty(prop); - assertEquals(Arrays.asList("com.aptsssss", "com.abc"), values); + public void testStringMultiPropertyDefaultDelimiter() { + Rule r = loadFirstRule( + "\n\n Desc\n" + + " \n" + + " Please move your class to the right folder(rest \nfolder)\n" + + " 2\n \n \n "); + Object propValue = r.getProperty(r.getPropertyDescriptor("packageRegEx")); + + assertEquals(Arrays.asList("com.aptsssss", "com.abc"), propValue); } @Test - public void testStringMultiPropertyDelimiter() throws Exception { + public void testStringMultiPropertyDelimiter() { Rule r = loadFirstRule("\n" + "\n " + " ruleset desc\n " + "\n" + + " instead.\" \n" + + "class=\"net.sourceforge.pmd.lang.rule.XPathRule\" language=\"dummy\">\n" + " Please move your class to the right folder(rest \nfolder)\n" + " 2\n \n \n" + " " + ""); - PropertyDescriptor> prop = (PropertyDescriptor>) r.getPropertyDescriptor("packageRegEx"); - List values = r.getProperty(prop); - assertEquals(Arrays.asList("com.aptsssss", "com.abc"), values); + + Object propValue = r.getProperty(r.getPropertyDescriptor("packageRegEx")); + assertEquals(Arrays.asList("com.aptsssss", "com.abc"), propValue); } @Test - public void testRuleSetWithDeprecatedRule() throws Exception { + public void testRuleSetWithDeprecatedRule() { RuleSet rs = loadRuleSet("\n" + "\n" - + " ruleset desc\n" - + " " - + ""); + + " ruleset desc\n" + + " " + + ""); assertEquals(1, rs.getRules().size()); Rule rule = rs.getRuleByName("DummyBasicMockRule"); assertNotNull(rule); @@ -228,11 +212,11 @@ public class RuleSetFactoryTest { * rule reference should be ignored, so at the end, we only have the new rule name in the ruleset. * This is because the deprecated reference points to a rule in the same ruleset. * - * @throws Exception */ @Test - public void testRuleSetWithDeprecatedButRenamedRule() throws Exception { - RuleSet rs = loadRuleSetWithDeprecationWarnings("\n" + "\n" + public void testRuleSetWithDeprecatedButRenamedRule() { + RuleSet rs = loadRuleSetWithDeprecationWarnings( + "\n" + "\n" + " ruleset desc\n" + " " + " " @@ -254,17 +238,19 @@ public class RuleSetFactoryTest { *

When loading this ruleset at a whole for generating the documentation, we should still * include the deprecated rule reference, so that we can create a nice documentation. * - * @throws Exception */ @Test - public void testRuleSetWithDeprecatedRenamedRuleForDoc() throws Exception { - RuleSetFactory rsf = RulesetsFactoryUtils.createFactory(RulePriority.LOW, false, false, true); - RuleSet rs = rsf.createRuleSet(createRuleSetReferenceId("\n" + "\n" - + " ruleset desc\n" - + " " - + " " - + " d\n" + " 2\n" + " " - + "")); + public void testRuleSetWithDeprecatedRenamedRuleForDoc() { + RuleSetLoader loader = new RuleSetLoader().includeDeprecatedRuleReferences(true); + RuleSet rs = loader.loadFromString("", + "\n" + "\n" + + " ruleset desc\n" + + " " + + " " + + " d\n" + + " 2\n" + + " " + + ""); assertEquals(2, rs.getRules().size()); assertNotNull(rs.getRuleByName("NewName")); assertNotNull(rs.getRuleByName("OldName")); @@ -273,12 +259,11 @@ public class RuleSetFactoryTest { /** * This is an example of a custom user ruleset, that references a rule, that has been renamed. * The user should get a deprecation warning. - * - * @throws Exception */ @Test - public void testRuleSetReferencesADeprecatedRenamedRule() throws Exception { - RuleSet rs = loadRuleSetWithDeprecationWarnings("\n" + "\n" + public void testRuleSetReferencesADeprecatedRenamedRule() { + RuleSet rs = loadRuleSetWithDeprecationWarnings( + "\n" + "\n" + " ruleset desc\n" + " " + ""); assertEquals(1, rs.getRules().size()); @@ -286,8 +271,8 @@ public class RuleSetFactoryTest { assertNotNull(rule); assertEquals(1, - StringUtils.countMatches(logging.getLog(), - "WARNING: Use Rule name rulesets/dummy/basic.xml/DummyBasicMockRule instead of the deprecated Rule name rulesets/dummy/basic.xml/OldNameOfDummyBasicMockRule.")); + StringUtils.countMatches(logging.getLog(), + "WARNING: Use Rule name rulesets/dummy/basic.xml/DummyBasicMockRule instead of the deprecated Rule name rulesets/dummy/basic.xml/OldNameOfDummyBasicMockRule.")); } /** @@ -303,11 +288,11 @@ public class RuleSetFactoryTest { *

* In the end, we should get all non-deprecated rules of the referenced ruleset. * - * @throws Exception */ @Test - public void testRuleSetReferencesRulesetWithADeprecatedRenamedRule() throws Exception { - RuleSet rs = loadRuleSetWithDeprecationWarnings("\n" + "\n" + public void testRuleSetReferencesRulesetWithADeprecatedRenamedRule() { + RuleSet rs = loadRuleSetWithDeprecationWarnings( + "\n" + "\n" + " ruleset desc\n" + " " + ""); assertEquals(2, rs.getRules().size()); @@ -331,13 +316,14 @@ public class RuleSetFactoryTest { *

* In the end, we should get all non-deprecated rules of the referenced ruleset. * - * @throws Exception */ @Test - public void testRuleSetReferencesRulesetWithAExcludedDeprecatedRule() throws Exception { - RuleSet rs = loadRuleSetWithDeprecationWarnings("\n" + "\n" + public void testRuleSetReferencesRulesetWithAExcludedDeprecatedRule() { + RuleSet rs = loadRuleSetWithDeprecationWarnings( + "\n" + "\n" + " ruleset desc\n" - + " " + ""); + + " " + + ""); assertEquals(2, rs.getRules().size()); assertNotNull(rs.getRuleByName("DummyBasicMockRule")); assertNotNull(rs.getRuleByName("SampleXPathRule")); @@ -355,20 +341,21 @@ public class RuleSetFactoryTest { * since not all rules are deprecated in the referenced ruleset. * Since the rule to be excluded doesn't exist, there should be a warning about that. * - * @throws Exception */ @Test - public void testRuleSetReferencesRulesetWithAExcludedNonExistingRule() throws Exception { - RuleSet rs = loadRuleSetWithDeprecationWarnings("\n" + "\n" + public void testRuleSetReferencesRulesetWithAExcludedNonExistingRule() { + RuleSet rs = loadRuleSetWithDeprecationWarnings( + "\n" + "\n" + " ruleset desc\n" - + " " + ""); + + " " + + ""); assertEquals(2, rs.getRules().size()); assertNotNull(rs.getRuleByName("DummyBasicMockRule")); assertNotNull(rs.getRuleByName("SampleXPathRule")); assertEquals(0, - StringUtils.countMatches(logging.getLog(), - "WARNING: Discontinue using Rule rulesets/dummy/basic.xml/DeprecatedRule as it is scheduled for removal from PMD.")); + StringUtils.countMatches(logging.getLog(), + "WARNING: Discontinue using Rule rulesets/dummy/basic.xml/DeprecatedRule as it is scheduled for removal from PMD.")); assertEquals(1, StringUtils.countMatches(logging.getLog(), "WARNING: Unable to exclude rules [NonExistingRule] from ruleset reference rulesets/dummy/basic.xml; perhaps the rule name is misspelled or the rule doesn't exist anymore?")); @@ -379,8 +366,9 @@ public class RuleSetFactoryTest { * considered deprecated and the user should get a deprecation warning for the ruleset. */ @Test - public void testRuleSetReferencesDeprecatedRuleset() throws Exception { - RuleSet rs = loadRuleSetWithDeprecationWarnings("\n" + "\n" + public void testRuleSetReferencesDeprecatedRuleset() { + RuleSet rs = loadRuleSetWithDeprecationWarnings( + "\n" + "\n" + " ruleset desc\n" + " " + ""); assertEquals(2, rs.getRules().size()); @@ -388,8 +376,8 @@ public class RuleSetFactoryTest { assertNotNull(rs.getRuleByName("SampleXPathRule")); assertEquals(1, - StringUtils.countMatches(logging.getLog(), - "WARNING: The RuleSet rulesets/dummy/deprecated.xml has been deprecated and will be removed in PMD")); + StringUtils.countMatches(logging.getLog(), + "WARNING: The RuleSet rulesets/dummy/deprecated.xml has been deprecated and will be removed in PMD")); } /** @@ -398,21 +386,22 @@ public class RuleSetFactoryTest { * no warning about deprecation - since the deprecated rules are not used. */ @Test - public void testRuleSetReferencesRulesetWithAMovedRule() throws Exception { - RuleSet rs = loadRuleSetWithDeprecationWarnings("\n" + "\n" + public void testRuleSetReferencesRulesetWithAMovedRule() { + RuleSet rs = loadRuleSetWithDeprecationWarnings( + "\n" + "\n" + " ruleset desc\n" + " " + ""); assertEquals(1, rs.getRules().size()); assertNotNull(rs.getRuleByName("DummyBasic2MockRule")); assertEquals(0, - StringUtils.countMatches(logging.getLog(), - "WARNING: Use Rule name rulesets/dummy/basic.xml/DummyBasicMockRule instead of the deprecated Rule name rulesets/dummy/basic2.xml/DummyBasicMockRule. PMD")); + StringUtils.countMatches(logging.getLog(), + "WARNING: Use Rule name rulesets/dummy/basic.xml/DummyBasicMockRule instead of the deprecated Rule name rulesets/dummy/basic2.xml/DummyBasicMockRule. PMD")); } @Test @SuppressWarnings("unchecked") - public void testXPath() throws RuleSetNotFoundException { + public void testXPath() { Rule r = loadFirstRule(XPATH); PropertyDescriptor xpathProperty = (PropertyDescriptor) r.getPropertyDescriptor("xpath"); assertNotNull("xpath property descriptor", xpathProperty); @@ -420,7 +409,7 @@ public class RuleSetFactoryTest { } @Test - public void testExternalReferenceOverride() throws RuleSetNotFoundException { + public void testExternalReferenceOverride() { Rule r = loadFirstRule(REF_OVERRIDE); assertEquals("TestNameOverride", r.getName()); assertEquals("Test message override", r.getMessage()); @@ -437,14 +426,18 @@ public class RuleSetFactoryTest { } @Test - public void testExternalReferenceOverrideNonExistent() throws RuleSetNotFoundException { - ex.expect(IllegalArgumentException.class); - ex.expectMessage("Cannot set non-existent property 'test4' on Rule TestNameOverride"); - loadFirstRule(REF_OVERRIDE_NONEXISTENT); + public void testExternalReferenceOverrideNonExistent() { + RuleSetLoadException ex = assertCannotParse(REF_OVERRIDE_NONEXISTENT); + + assertThat(ex.getCause().getMessage(), containsString("Cannot set non-existent property 'test4' on Rule TestNameOverride")); + } + + private RuleSetLoadException assertCannotParse(String xmlContent) { + return assertThrows(RuleSetLoadException.class, () -> loadFirstRule(xmlContent)); } @Test - public void testReferenceInternalToInternal() throws RuleSetNotFoundException { + public void testReferenceInternalToInternal() { RuleSet ruleSet = loadRuleSet(REF_INTERNAL_TO_INTERNAL); Rule rule = ruleSet.getRuleByName("MockRuleName"); @@ -455,7 +448,7 @@ public class RuleSetFactoryTest { } @Test - public void testReferenceInternalToInternalChain() throws RuleSetNotFoundException { + public void testReferenceInternalToInternalChain() { RuleSet ruleSet = loadRuleSet(REF_INTERNAL_TO_INTERNAL_CHAIN); Rule rule = ruleSet.getRuleByName("MockRuleName"); @@ -469,7 +462,7 @@ public class RuleSetFactoryTest { } @Test - public void testReferenceInternalToExternal() throws RuleSetNotFoundException { + public void testReferenceInternalToExternal() { RuleSet ruleSet = loadRuleSet(REF_INTERNAL_TO_EXTERNAL); Rule rule = ruleSet.getRuleByName("ExternalRefRuleName"); @@ -480,7 +473,7 @@ public class RuleSetFactoryTest { } @Test - public void testReferenceInternalToExternalChain() throws RuleSetNotFoundException { + public void testReferenceInternalToExternalChain() { RuleSet ruleSet = loadRuleSet(REF_INTERNAL_TO_EXTERNAL_CHAIN); Rule rule = ruleSet.getRuleByName("ExternalRefRuleName"); @@ -494,50 +487,50 @@ public class RuleSetFactoryTest { } @Test - public void testReferencePriority() throws RuleSetNotFoundException { + public void testReferencePriority() { RuleSetLoader config = new RuleSetLoader().warnDeprecated(false).enableCompatibility(true); - RuleSetFactory rsf = config.filterAbovePriority(RulePriority.LOW).toFactory(); - RuleSet ruleSet = rsf.createRuleSet(createRuleSetReferenceId(REF_INTERNAL_TO_INTERNAL_CHAIN)); + RuleSetLoader rsf = config.filterAbovePriority(RulePriority.LOW); + RuleSet ruleSet = rsf.loadFromString("", REF_INTERNAL_TO_INTERNAL_CHAIN); assertEquals("Number of Rules", 3, ruleSet.getRules().size()); assertNotNull(ruleSet.getRuleByName("MockRuleName")); assertNotNull(ruleSet.getRuleByName("MockRuleNameRef")); assertNotNull(ruleSet.getRuleByName("MockRuleNameRefRef")); - rsf = config.filterAbovePriority(RulePriority.MEDIUM_HIGH).toFactory(); - ruleSet = rsf.createRuleSet(createRuleSetReferenceId(REF_INTERNAL_TO_INTERNAL_CHAIN)); + rsf = config.filterAbovePriority(RulePriority.MEDIUM_HIGH); + ruleSet = rsf.loadFromString("", REF_INTERNAL_TO_INTERNAL_CHAIN); assertEquals("Number of Rules", 2, ruleSet.getRules().size()); assertNotNull(ruleSet.getRuleByName("MockRuleNameRef")); assertNotNull(ruleSet.getRuleByName("MockRuleNameRefRef")); - rsf = config.filterAbovePriority(RulePriority.HIGH).toFactory(); - ruleSet = rsf.createRuleSet(createRuleSetReferenceId(REF_INTERNAL_TO_INTERNAL_CHAIN)); + rsf = config.filterAbovePriority(RulePriority.HIGH); + ruleSet = rsf.loadFromString("", REF_INTERNAL_TO_INTERNAL_CHAIN); assertEquals("Number of Rules", 1, ruleSet.getRules().size()); assertNotNull(ruleSet.getRuleByName("MockRuleNameRefRef")); - rsf = config.filterAbovePriority(RulePriority.LOW).toFactory(); - ruleSet = rsf.createRuleSet(createRuleSetReferenceId(REF_INTERNAL_TO_EXTERNAL_CHAIN)); + rsf = config.filterAbovePriority(RulePriority.LOW); + ruleSet = rsf.loadFromString("", REF_INTERNAL_TO_EXTERNAL_CHAIN); assertEquals("Number of Rules", 3, ruleSet.getRules().size()); assertNotNull(ruleSet.getRuleByName("ExternalRefRuleName")); assertNotNull(ruleSet.getRuleByName("ExternalRefRuleNameRef")); assertNotNull(ruleSet.getRuleByName("ExternalRefRuleNameRefRef")); - rsf = config.filterAbovePriority(RulePriority.MEDIUM_HIGH).toFactory(); - ruleSet = rsf.createRuleSet(createRuleSetReferenceId(REF_INTERNAL_TO_EXTERNAL_CHAIN)); + rsf = config.filterAbovePriority(RulePriority.MEDIUM_HIGH); + ruleSet = rsf.loadFromString("", REF_INTERNAL_TO_EXTERNAL_CHAIN); assertEquals("Number of Rules", 2, ruleSet.getRules().size()); assertNotNull(ruleSet.getRuleByName("ExternalRefRuleNameRef")); assertNotNull(ruleSet.getRuleByName("ExternalRefRuleNameRefRef")); - rsf = config.filterAbovePriority(RulePriority.HIGH).toFactory(); - ruleSet = rsf.createRuleSet(createRuleSetReferenceId(REF_INTERNAL_TO_EXTERNAL_CHAIN)); + rsf = config.filterAbovePriority(RulePriority.HIGH); + ruleSet = rsf.loadFromString("", REF_INTERNAL_TO_EXTERNAL_CHAIN); assertEquals("Number of Rules", 1, ruleSet.getRules().size()); assertNotNull(ruleSet.getRuleByName("ExternalRefRuleNameRefRef")); } @Test - public void testOverridePriorityLoadWithMinimum() throws RuleSetNotFoundException { - RuleSetFactory rsf = new RuleSetLoader().filterAbovePriority(RulePriority.MEDIUM_LOW).warnDeprecated(true).enableCompatibility(true).toFactory(); - RuleSet ruleset = rsf.createRuleSet("net/sourceforge/pmd/rulesets/ruleset-minimum-priority.xml"); + public void testOverridePriorityLoadWithMinimum() { + RuleSetLoader rsf = new RuleSetLoader().filterAbovePriority(RulePriority.MEDIUM_LOW).warnDeprecated(true).enableCompatibility(true); + RuleSet ruleset = rsf.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("Number of Rules", 1, ruleset.getRules().size()); @@ -548,23 +541,23 @@ public class RuleSetFactoryTest { assertNotNull(ruleset.getRuleByName("SampleXPathRule")); // now, load with default minimum priority - rsf = RulesetsFactoryUtils.defaultFactory(); - ruleset = rsf.createRuleSet("net/sourceforge/pmd/rulesets/ruleset-minimum-priority.xml"); + rsf = new RuleSetLoader(); + ruleset = rsf.loadFromResource("net/sourceforge/pmd/rulesets/ruleset-minimum-priority.xml"); assertEquals("Number of Rules", 2, ruleset.getRules().size()); Rule dummyBasicMockRule = ruleset.getRuleByName("DummyBasicMockRule"); assertEquals("Wrong Priority", RulePriority.LOW, dummyBasicMockRule.getPriority()); } @Test - public void testExcludeWithMinimumPriority() throws RuleSetNotFoundException { - RuleSetFactory rsf = new RuleSetLoader().filterAbovePriority(RulePriority.HIGH).toFactory(); - RuleSet ruleset = rsf.createRuleSet("net/sourceforge/pmd/rulesets/ruleset-minimum-priority-exclusion.xml"); + public void testExcludeWithMinimumPriority() { + RuleSetLoader rsf = new RuleSetLoader().filterAbovePriority(RulePriority.HIGH); + RuleSet ruleset = rsf.loadFromResource("net/sourceforge/pmd/rulesets/ruleset-minimum-priority-exclusion.xml"); // no rules should be loaded assertEquals("Number of Rules", 0, ruleset.getRules().size()); // now, load with default minimum priority - rsf = new RuleSetLoader().filterAbovePriority(RulePriority.LOW).toFactory(); - ruleset = rsf.createRuleSet("net/sourceforge/pmd/rulesets/ruleset-minimum-priority-exclusion.xml"); + rsf = new RuleSetLoader().filterAbovePriority(RulePriority.LOW); + ruleset = rsf.loadFromResource("net/sourceforge/pmd/rulesets/ruleset-minimum-priority-exclusion.xml"); // only one rule, we have excluded one... assertEquals("Number of Rules", 1, ruleset.getRules().size()); // rule is excluded @@ -574,100 +567,101 @@ public class RuleSetFactoryTest { } @Test - public void testOverrideMessage() throws RuleSetNotFoundException { + public void testOverrideMessage() { Rule r = loadFirstRule(REF_OVERRIDE_ORIGINAL_NAME); assertEquals("TestMessageOverride", r.getMessage()); } @Test - public void testOverrideMessageOneElem() throws RuleSetNotFoundException { + public void testOverrideMessageOneElem() { Rule r = loadFirstRule(REF_OVERRIDE_ORIGINAL_NAME_ONE_ELEM); assertEquals("TestMessageOverride", r.getMessage()); } - @Test(expected = IllegalArgumentException.class) - public void testIncorrectExternalRef() throws IllegalArgumentException, RuleSetNotFoundException { - loadFirstRule(REF_MISSPELLED_XREF); + @Test + public void testIncorrectExternalRef() { + assertCannotParse(REF_MISSPELLED_XREF); } @Test - public void testSetPriority() throws RuleSetNotFoundException { - RuleSetFactory rsf = new RuleSetLoader().filterAbovePriority(RulePriority.MEDIUM_HIGH).warnDeprecated(false).toFactory(); - assertEquals(0, rsf.createRuleSet(createRuleSetReferenceId(SINGLE_RULE)).size()); - rsf = new RuleSetLoader().filterAbovePriority(RulePriority.MEDIUM_LOW).warnDeprecated(false).toFactory(); - assertEquals(1, rsf.createRuleSet(createRuleSetReferenceId(SINGLE_RULE)).size()); + public void testSetPriority() { + RuleSetLoader rsf = new RuleSetLoader().filterAbovePriority(RulePriority.MEDIUM_HIGH).warnDeprecated(false); + assertEquals(0, rsf.loadFromString("", SINGLE_RULE).size()); + rsf = new RuleSetLoader().filterAbovePriority(RulePriority.MEDIUM_LOW).warnDeprecated(false); + assertEquals(1, rsf.loadFromString("", SINGLE_RULE).size()); } @Test - public void testLanguage() throws RuleSetNotFoundException { + public void testLanguage() { Rule r = loadFirstRule(LANGUAGE); assertEquals(LanguageRegistry.getLanguage(DummyLanguageModule.NAME), r.getLanguage()); } - @Test(expected = IllegalArgumentException.class) - public void testIncorrectLanguage() throws RuleSetNotFoundException { - loadFirstRule(INCORRECT_LANGUAGE); + @Test + public void testIncorrectLanguage() { + assertCannotParse(INCORRECT_LANGUAGE); } @Test - public void testMinimumLanguageVersion() throws RuleSetNotFoundException { + public void testMinimumLanguageVersion() { Rule r = loadFirstRule(MINIMUM_LANGUAGE_VERSION); assertEquals(LanguageRegistry.getLanguage(DummyLanguageModule.NAME).getVersion("1.4"), - r.getMinimumLanguageVersion()); + r.getMinimumLanguageVersion()); } @Test - public void testIncorrectMinimumLanguageVersion() throws RuleSetNotFoundException { - ex.expect(IllegalArgumentException.class); - ex.expectMessage(Matchers.containsString("1.0, 1.1, 1.2")); // and not "dummy 1.0, dummy 1.1, ..." - loadFirstRule(INCORRECT_MINIMUM_LANGUAGE_VERSION); - } + public void testIncorrectMinimumLanguageVersion() { + RuleSetLoadException ex = assertCannotParse(INCORRECT_MINIMUM_LANGUAGE_VERSION); + + assertThat(ex.getCause().getMessage(), containsString("1.0, 1.1, 1.2")); // and not "dummy 1.0, dummy 1.1, ..." - @Test(expected = IllegalArgumentException.class) - public void testIncorrectMinimumLanugageVersionWithLanguageSetInJava() throws RuleSetNotFoundException { - loadFirstRule("\n" - + "\n" - + " TODO\n" - + "\n" - + " \n" - + " TODO\n" - + " 2\n" - + " \n" - + "\n" - + ""); } @Test - public void testMaximumLanguageVersion() throws RuleSetNotFoundException { + public void testIncorrectMinimumLanguageVersionWithLanguageSetInJava() { + assertCannotParse("\n" + + "\n" + + " TODO\n" + + "\n" + + " \n" + + " TODO\n" + + " 2\n" + + " \n" + + "\n" + + ""); + } + + @Test + public void testMaximumLanguageVersion() { Rule r = loadFirstRule(MAXIMUM_LANGUAGE_VERSION); assertEquals(LanguageRegistry.getLanguage(DummyLanguageModule.NAME).getVersion("1.7"), - r.getMaximumLanguageVersion()); + r.getMaximumLanguageVersion()); } @Test - public void testIncorrectMaximumLanguageVersion() throws RuleSetNotFoundException { - ex.expect(IllegalArgumentException.class); - ex.expectMessage(Matchers.containsString("1.0, 1.1, 1.2")); // and not "dummy 1.0, dummy 1.1, ..." - loadFirstRule(INCORRECT_MAXIMUM_LANGUAGE_VERSION); - } + public void testIncorrectMaximumLanguageVersion() { + RuleSetLoadException ex = assertCannotParse(INCORRECT_MAXIMUM_LANGUAGE_VERSION); - @Test(expected = IllegalArgumentException.class) - public void testInvertedMinimumMaximumLanguageVersions() throws RuleSetNotFoundException { - loadFirstRule(INVERTED_MINIMUM_MAXIMUM_LANGUAGE_VERSIONS); + assertThat(ex.getCause().getMessage(), containsString("1.0, 1.1, 1.2")); // and not "dummy 1.0, dummy 1.1, ..." } @Test - public void testDirectDeprecatedRule() throws RuleSetNotFoundException { + public void testInvertedMinimumMaximumLanguageVersions() { + assertCannotParse(INCORRECT_MAXIMUM_LANGUAGE_VERSION); + } + + @Test + public void testDirectDeprecatedRule() { Rule r = loadFirstRule(DIRECT_DEPRECATED_RULE); assertNotNull("Direct Deprecated Rule", r); assertTrue(r.isDeprecated()); } @Test - public void testReferenceToDeprecatedRule() throws RuleSetNotFoundException { + public void testReferenceToDeprecatedRule() { Rule r = loadFirstRule(REFERENCE_TO_DEPRECATED_RULE); assertNotNull("Reference to Deprecated Rule", r); assertTrue("Rule Reference", r instanceof RuleReference); @@ -677,7 +671,7 @@ public class RuleSetFactoryTest { } @Test - public void testRuleSetReferenceWithDeprecatedRule() throws RuleSetNotFoundException { + public void testRuleSetReferenceWithDeprecatedRule() { RuleSet ruleSet = loadRuleSet(REFERENCE_TO_RULESET_WITH_DEPRECATED_RULE); assertNotNull("RuleSet", ruleSet); assertFalse("RuleSet empty", ruleSet.getRules().isEmpty()); @@ -691,90 +685,77 @@ public class RuleSetFactoryTest { } @Test - public void testDeprecatedRuleSetReference() throws RuleSetNotFoundException { - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); - RuleSet ruleSet = ruleSetFactory.createRuleSet("net/sourceforge/pmd/rulesets/ruleset-deprecated.xml"); + public void testDeprecatedRuleSetReference() { + RuleSet ruleSet = new RuleSetLoader().loadFromResource("net/sourceforge/pmd/rulesets/ruleset-deprecated.xml"); assertEquals(2, ruleSet.getRules().size()); } @Test - public void testExternalReferences() throws RuleSetNotFoundException { + public void testExternalReferences() { RuleSet rs = loadRuleSet(EXTERNAL_REFERENCE_RULE_SET); assertEquals(1, rs.size()); assertEquals(MockRule.class.getName(), rs.getRuleByName("MockRule").getRuleClass()); } @Test - public void testIncludeExcludePatterns() throws RuleSetNotFoundException { + public void testIncludeExcludePatterns() { RuleSet ruleSet = loadRuleSet(INCLUDE_EXCLUDE_RULESET); - assertNotNull("Include patterns", ruleSet.getIncludePatterns()); - assertEquals("Include patterns size", 2, ruleSet.getIncludePatterns().size()); - assertEquals("Include pattern #1", "include1", ruleSet.getIncludePatterns().get(0)); - assertEquals("Include pattern #2", "include2", ruleSet.getIncludePatterns().get(1)); + assertNotNull("Include patterns", ruleSet.getFileInclusions()); + assertEquals("Include patterns size", 2, ruleSet.getFileInclusions().size()); + assertEquals("Include pattern #1", "include1", ruleSet.getFileInclusions().get(0).pattern()); + assertEquals("Include pattern #2", "include2", ruleSet.getFileInclusions().get(1).pattern()); - assertNotNull("Exclude patterns", ruleSet.getExcludePatterns()); - assertEquals("Exclude patterns size", 3, ruleSet.getExcludePatterns().size()); - assertEquals("Exclude pattern #1", "exclude1", ruleSet.getExcludePatterns().get(0)); - assertEquals("Exclude pattern #2", "exclude2", ruleSet.getExcludePatterns().get(1)); - assertEquals("Exclude pattern #3", "exclude3", ruleSet.getExcludePatterns().get(2)); + assertNotNull("Exclude patterns", ruleSet.getFileExclusions()); + assertEquals("Exclude patterns size", 3, ruleSet.getFileExclusions().size()); + assertEquals("Exclude pattern #1", "exclude1", ruleSet.getFileExclusions().get(0).pattern()); + assertEquals("Exclude pattern #2", "exclude2", ruleSet.getFileExclusions().get(1).pattern()); + assertEquals("Exclude pattern #3", "exclude3", ruleSet.getFileExclusions().get(2).pattern()); } /** * Rule reference can't be resolved - ref is used instead of class and the * class is old (pmd 4.3 and not pmd 5). - * - * @throws Exception - * any error */ - @Test(expected = RuleSetNotFoundException.class) - public void testBug1202() throws Exception { - RuleSetReferenceId ref = createRuleSetReferenceId("\n" + "\n" + @Test + public void testBug1202() { + Assert.assertThrows( + RuleSetLoadException.class, + () -> new RuleSetLoader().loadFromString("", "\n" + "\n" + " \n" + " 1\n" + " \n" + " \n" + " \n" + " \n" + " \n" - + "\n"); - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); - ruleSetFactory.createRuleSet(ref); + + "\n") + ); } /** * See https://sourceforge.net/p/pmd/bugs/1225/ - * - * @throws Exception - * any error */ @Test - public void testEmptyRuleSetFile() throws Exception { - RuleSetReferenceId ref = createRuleSetReferenceId("\n" + "\n" - + "\n" - + " PMD Ruleset.\n" + "\n" - + " .*Test.*\n" + "\n" + "\n"); - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); - RuleSet ruleset = ruleSetFactory.createRuleSet(ref); + public void testEmptyRuleSetFile() { + RuleSet ruleset = new RuleSetLoader().loadFromString("", "\n" + "\n" + + "\n" + + " PMD Ruleset.\n" + "\n" + + " .*Test.*\n" + "\n" + "\n"); assertEquals(0, ruleset.getRules().size()); } /** * See https://github.com/pmd/pmd/issues/782 * Empty ruleset should be interpreted as deprecated. - * - * @throws Exception - * any error */ @Test - public void testEmptyRuleSetReferencedShouldNotBeDeprecated() throws Exception { - RuleSetReferenceId ref = createRuleSetReferenceId("\n" + "\n" - + "\n" - + " Ruleset which references a empty ruleset\n" + "\n" - + " \n" - + "\n"); - RuleSetFactory ruleSetFactory = new RuleSetLoader().loadResourcesWith(new ResourceLoader()).filterAbovePriority(RulePriority.LOW).warnDeprecated(true).enableCompatibility(true).toFactory(); - RuleSet ruleset = ruleSetFactory.createRuleSet(ref); + public void testEmptyRuleSetReferencedShouldNotBeDeprecated() { + RuleSet ruleset = new RuleSetLoader().loadFromString("", "\n" + "\n" + + "\n" + + " Ruleset which references a empty ruleset\n" + "\n" + + " \n" + + "\n"); assertEquals(0, ruleset.getRules().size()); assertTrue(logging.getLog().isEmpty()); @@ -782,43 +763,36 @@ public class RuleSetFactoryTest { /** * See https://sourceforge.net/p/pmd/bugs/1231/ - * - * @throws Exception - * any error */ - @Test(expected = IllegalArgumentException.class) - public void testWrongRuleNameReferenced() throws Exception { - RuleSetReferenceId ref = createRuleSetReferenceId("\n" - + "\n" - + " Custom ruleset for tests\n" - + " \n" + "\n"); - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); - ruleSetFactory.createRuleSet(ref); + @Test + public void testWrongRuleNameReferenced() { + assertCannotParse("\n" + + "\n" + + " Custom ruleset for tests\n" + + " \n" + + "\n"); } /** * Unit test for #1312 see https://sourceforge.net/p/pmd/bugs/1312/ * - * @throws Exception - * any error */ @Test - public void testRuleReferenceWithNameOverridden() throws Exception { - RuleSetReferenceId ref = createRuleSetReferenceId("\n" - + "\n" - + " PMD Plugin preferences rule set\n" + "\n" - + "\n" + "\n" + "\n" + ""); - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); - RuleSet rs = ruleSetFactory.createRuleSet(ref); + public void testRuleReferenceWithNameOverridden() { + RuleSet rs = loadRuleSet("\n" + + "\n" + + " PMD Plugin preferences rule set\n" + + "\n" + "\n" + "\n" + + ""); - Rule r = rs.getRules().toArray(new Rule[1])[0]; + Rule r = rs.getRules().iterator().next(); assertEquals("OverriddenDummyBasicMockRule", r.getName()); RuleReference ruleRef = (RuleReference) r; assertEquals("DummyBasicMockRule", ruleRef.getRule().getName()); @@ -829,21 +803,17 @@ public class RuleSetFactoryTest { * *

See https://github.com/pmd/pmd/issues/1978 - with that, it should not be an error anymore. * - * @throws Exception - * any error */ @Test - public void testWrongRuleNameExcluded() throws Exception { - RuleSetReferenceId ref = createRuleSetReferenceId( - "\n" + "\n" - + " Custom ruleset for tests\n" - + " \n" - + " \n" + " \n" + "\n"); - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); - RuleSet ruleset = ruleSetFactory.createRuleSet(ref); + public void testWrongRuleNameExcluded() { + RuleSet ruleset = loadRuleSet("\n" + "\n" + + " Custom ruleset for tests\n" + + " \n" + + " \n" + " \n" + + "\n"); assertEquals(4, ruleset.getRules().size()); } @@ -854,8 +824,6 @@ public class RuleSetFactoryTest { * Currently, if a ruleset is imported twice, the excludes of the first * import are ignored. Duplicated rules are silently ignored. * - * @throws Exception - * any error * @see #1537 Implement * strict ruleset parsing * @see */ @Test - public void testExcludeAndImportTwice() throws Exception { - RuleSetReferenceId ref1 = createRuleSetReferenceId( - "\n" + "\n" - + " Custom ruleset for tests\n" - + " \n" + " \n" - + " \n" + "\n"); - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); - RuleSet ruleset = ruleSetFactory.createRuleSet(ref1); + public void testExcludeAndImportTwice() { + RuleSet ruleset = loadRuleSet("\n" + "\n" + + " Custom ruleset for tests\n" + + " \n" + + " \n" + + " \n" + "\n"); assertNull(ruleset.getRuleByName("DummyBasicMockRule")); - RuleSetReferenceId ref2 = createRuleSetReferenceId( - "\n" + "\n" - + " Custom ruleset for tests\n" - + " \n" + " \n" - + " \n" + " \n" + "\n"); - RuleSetFactory ruleSetFactory2 = RulesetsFactoryUtils.defaultFactory(); - RuleSet ruleset2 = ruleSetFactory2.createRuleSet(ref2); + RuleSet ruleset2 = loadRuleSet("\n" + "\n" + + " Custom ruleset for tests\n" + + " \n" + + " \n" + + " \n" + " \n" + + "\n"); assertNotNull(ruleset2.getRuleByName("DummyBasicMockRule")); - RuleSetReferenceId ref3 = createRuleSetReferenceId( - "\n" + "\n" - + " Custom ruleset for tests\n" - + " \n" + " \n" - + " \n" + " \n" + "\n"); - RuleSetFactory ruleSetFactory3 = RulesetsFactoryUtils.defaultFactory(); - RuleSet ruleset3 = ruleSetFactory3.createRuleSet(ref3); + RuleSet ruleset3 = loadRuleSet("\n" + "\n" + + " Custom ruleset for tests\n" + + " \n" + + " \n" + + " \n" + " \n" + + "\n"); assertNotNull(ruleset3.getRuleByName("DummyBasicMockRule")); } - @org.junit.Rule - public JavaUtilLoggingRule logging = new JavaUtilLoggingRule(RuleSetFactory.class.getName()); - @Test - public void testMissingRuleSetNameIsWarning() throws Exception { - RuleSetReferenceId ref = createRuleSetReferenceId( - "\n" + "\n" - + " Custom ruleset for tests\n" - + " \n" - + " \n"); - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); - ruleSetFactory.createRuleSet(ref); + public void testMissingRuleSetNameIsWarning() { + loadRuleSetWithDeprecationWarnings( + "\n" + "\n" + + " Custom ruleset for tests\n" + + " \n" + + " \n" + ); assertTrue(logging.getLog().contains("RuleSet name is missing.")); } @Test - public void testMissingRuleSetDescriptionIsWarning() throws Exception { - RuleSetReferenceId ref = createRuleSetReferenceId( - "\n" + "\n" - + " \n" - + " \n"); - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); - ruleSetFactory.createRuleSet(ref); + public void testMissingRuleSetDescriptionIsWarning() { + loadRuleSetWithDeprecationWarnings( + "\n" + "\n" + + " \n" + + " \n" + ); assertTrue(logging.getLog().contains("RuleSet description is missing.")); } @@ -1267,27 +1226,17 @@ public class RuleSetFactoryTest { + "\n" + ""; - private Rule loadFirstRule(String ruleSetXml) throws RuleSetNotFoundException { + private Rule loadFirstRule(String ruleSetXml) { RuleSet rs = loadRuleSet(ruleSetXml); return rs.getRules().iterator().next(); } - private RuleSet loadRuleSet(String ruleSetXml) throws RuleSetNotFoundException { - RuleSetFactory rsf = RulesetsFactoryUtils.defaultFactory(); - return rsf.createRuleSet(createRuleSetReferenceId(ruleSetXml)); + private RuleSet loadRuleSet(String ruleSetXml) { + return new RuleSetLoader().loadFromString("dummyRuleset.xml", ruleSetXml); } - private RuleSet loadRuleSetWithDeprecationWarnings(String ruleSetXml) throws RuleSetNotFoundException { - RuleSetFactory rsf = new RuleSetLoader().warnDeprecated(true).enableCompatibility(false).toFactory(); - return rsf.createRuleSet(createRuleSetReferenceId(ruleSetXml)); + private RuleSet loadRuleSetWithDeprecationWarnings(String ruleSetXml) { + return new RuleSetLoader().warnDeprecated(true).enableCompatibility(false).loadFromString("testRuleset.xml", ruleSetXml); } - private static RuleSetReferenceId createRuleSetReferenceId(final String ruleSetXml) { - return new RuleSetReferenceId(null) { - @Override - public InputStream getInputStream(ResourceLoader resourceLoader) throws RuleSetNotFoundException { - return new ByteArrayInputStream(ruleSetXml.getBytes(StandardCharsets.UTF_8)); - } - }; - } } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetSchemaTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetSchemaTest.java index aeb18f1369..a07b476176 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetSchemaTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetSchemaTest.java @@ -106,7 +106,7 @@ public class RuleSetSchemaTest { } public static class PMDRuleSetEntityResolver implements EntityResolver { - private static URL schema2 = RuleSetFactory.class.getResource("/ruleset_2_0_0.xsd"); + private static URL schema2 = PMDRuleSetEntityResolver.class.getResource("/ruleset_2_0_0.xsd"); private static SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); @Override diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetTest.java index 374baa6da5..5a67ff65d0 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetTest.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Random; import java.util.Set; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.apache.commons.io.FilenameUtils; import org.checkerframework.checker.nullness.qual.NonNull; @@ -269,8 +270,8 @@ public class RuleSetTest { createRuleSetBuilder("ruleset1") .withFileExclusions(Pattern.compile(".*")) .build(); - assertNotNull("Exclude patterns", ruleSet.getExcludePatterns()); - assertEquals("Invalid number of patterns", 1, ruleSet.getExcludePatterns().size()); + assertNotNull("Exclude patterns", ruleSet.getFileExclusions()); + assertEquals("Invalid number of patterns", 1, ruleSet.getFileExclusions().size()); } @Test @@ -280,24 +281,28 @@ public class RuleSetTest { .withFileExclusions(Pattern.compile(".*")) .withFileExclusions(Pattern.compile(".*ha")) .build(); - assertEquals("Exclude pattern", Arrays.asList(".*", ".*ha"), ruleSet2.getExcludePatterns()); + assertEquals("Exclude pattern", Arrays.asList(".*", ".*ha"), toStrings(ruleSet2.getFileExclusions())); } @Test public void testIncludePatternsAreOrdered() { RuleSet ruleSet2 = createRuleSetBuilder("ruleset2") - .withFileInclusions(Pattern.compile(".*")) - .withFileInclusions(Arrays.asList(Pattern.compile(".*ha"), Pattern.compile(".*hb"))) - .build(); - assertEquals("Exclude pattern", Arrays.asList(".*", ".*ha", ".*hb"), ruleSet2.getIncludePatterns()); + .withFileInclusions(Pattern.compile(".*")) + .withFileInclusions(Arrays.asList(Pattern.compile(".*ha"), Pattern.compile(".*hb"))) + .build(); + assertEquals("Exclude pattern", Arrays.asList(".*", ".*ha", ".*hb"), toStrings(ruleSet2.getFileInclusions())); + } + + private List toStrings(List strings) { + return strings.stream().map(Pattern::pattern).collect(Collectors.toList()); } @Test public void testAddExcludePatterns() { RuleSet ruleSet = createRuleSetBuilder("ruleset1") - .withFileExclusions(Pattern.compile(".*")) - .build(); + .withFileExclusions(Pattern.compile(".*")) + .build(); assertNotNull("Exclude patterns", ruleSet.getFileExclusions()); assertEquals("Invalid number of patterns", 1, ruleSet.getFileExclusions().size()); @@ -315,7 +320,6 @@ public class RuleSetTest { excludePatterns.add(Pattern.compile("ah*")); excludePatterns.add(Pattern.compile(".*")); RuleSet ruleSet = createRuleSetBuilder("ruleset").replaceFileExclusions(excludePatterns).build(); - assertNotNull("Exclude patterns", ruleSet.getExcludePatterns()); assertNotNull("Exclude patterns", ruleSet.getFileExclusions()); assertEquals("Invalid number of exclude patterns", 2, ruleSet.getFileExclusions().size()); assertEquals("Exclude pattern", "ah*", ruleSet.getFileExclusions().get(0).pattern()); diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetWriterTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetWriterTest.java index 6f6e1643dc..63bc53a909 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetWriterTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetWriterTest.java @@ -51,7 +51,7 @@ public class RuleSetWriterTest { */ @Test public void testWrite() throws Exception { - RuleSet braces = RulesetsFactoryUtils.defaultFactory().createRuleSet("net/sourceforge/pmd/TestRuleset1.xml"); + RuleSet braces = new RuleSetLoader().loadFromResource("net/sourceforge/pmd/TestRuleset1.xml"); RuleSet ruleSet = new RuleSetBuilder(new Random().nextLong()) .withName("ruleset") .withDescription("ruleset description") @@ -72,13 +72,11 @@ public class RuleSetWriterTest { */ @Test public void testRuleReferenceOverriddenName() throws Exception { - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); - RuleSet rs = ruleSetFactory.createRuleSet("dummy-basic"); - RuleSetReference ruleSetReference = new RuleSetReference("rulesets/dummy/basic.xml"); + RuleSet rs = new RuleSetLoader().loadFromResource("rulesets/dummy/basic.xml"); RuleReference ruleRef = new RuleReference(); ruleRef.setRule(rs.getRuleByName("DummyBasicMockRule")); - ruleRef.setRuleSetReference(ruleSetReference); + ruleRef.setRuleSetReference(new RuleSetReference("rulesets/dummy/basic.xml")); ruleRef.setName("Foo"); // override the name RuleSet ruleSet = RuleSet.forSingleRule(ruleRef); diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/SourceCodeProcessorTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/SourceCodeProcessorTest.java index 1827d2a49f..6c8a17e462 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/SourceCodeProcessorTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/SourceCodeProcessorTest.java @@ -43,7 +43,7 @@ public class SourceCodeProcessorTest { processor = new SourceCodeProcessor(new PMDConfiguration()); sourceCode = new StringReader("test"); Rule rule = new RuleThatThrows(); - rulesets = Arrays.asList(RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(rule)); + rulesets = Arrays.asList(RuleSet.forSingleRule(rule)); ctx = new RuleContext(); } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/internal/StageDependencyTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/internal/StageDependencyTest.java index 7b0ba1fc59..7d4d8595af 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/internal/StageDependencyTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/internal/StageDependencyTest.java @@ -15,7 +15,6 @@ import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.Rule; import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RuleSetFactory; import net.sourceforge.pmd.RuleSets; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageRegistry; @@ -140,9 +139,9 @@ public class StageDependencyTest { private static RuleSets withRules(Rule r, Rule... rs) { List rsets = new ArrayList<>(); - rsets.add(new RuleSetFactory().createSingleRuleRuleSet(r)); + rsets.add(RuleSet.forSingleRule(r)); for (Rule rule : rs) { - rsets.add(new RuleSetFactory().createSingleRuleRuleSet(rule)); + rsets.add(RuleSet.forSingleRule(rule)); } return new RuleSets(rsets); diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/processor/MultiThreadProcessorTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/processor/MultiThreadProcessorTest.java index e0a1025e8d..67b82de86f 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/processor/MultiThreadProcessorTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/processor/MultiThreadProcessorTest.java @@ -21,9 +21,9 @@ import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.Report; import net.sourceforge.pmd.Report.ConfigurationError; import net.sourceforge.pmd.RuleContext; -import net.sourceforge.pmd.RuleSetFactory; +import net.sourceforge.pmd.RuleSetLoader; +import net.sourceforge.pmd.RuleSets; import net.sourceforge.pmd.RuleViolation; -import net.sourceforge.pmd.RulesetsFactoryUtils; import net.sourceforge.pmd.ThreadSafeReportListener; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.rule.AbstractRule; @@ -36,13 +36,12 @@ public class MultiThreadProcessorTest { private RuleContext ctx; private MultiThreadProcessor processor; - private RuleSetFactory ruleSetFactory; + private RuleSets ruleSets; private List files; private SimpleReportListener reportListener; public void setUpForTest(final String ruleset) { PMDConfiguration configuration = new PMDConfiguration(); - configuration.setRuleSets(ruleset); configuration.setThreads(2); files = new ArrayList<>(); files.add(new StringDataSource("file1-violation.dummy", "ABC")); @@ -53,7 +52,7 @@ public class MultiThreadProcessorTest { ctx.getReport().addListener(reportListener); processor = new MultiThreadProcessor(configuration); - ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); + ruleSets = new RuleSets(new RuleSetLoader().loadFromResources(ruleset)); } @Test @@ -61,7 +60,7 @@ public class MultiThreadProcessorTest { setUpForTest("rulesets/MultiThreadProcessorTest/dysfunctional.xml"); final SimpleRenderer renderer = new SimpleRenderer(null, null); renderer.start(); - processor.processFiles(ruleSetFactory, files, ctx, Collections.singletonList(renderer)); + processor.processFiles(ruleSets, files, ctx, Collections.singletonList(renderer)); renderer.end(); final Iterator configErrors = renderer.getReport().getConfigurationErrors().iterator(); @@ -77,7 +76,7 @@ public class MultiThreadProcessorTest { @Test public void testRulesThreadSafety() { setUpForTest("rulesets/MultiThreadProcessorTest/basic.xml"); - processor.processFiles(ruleSetFactory, files, ctx, Collections.emptyList()); + processor.processFiles(ruleSets, files, ctx, Collections.emptyList()); // if the rule is not executed, then maybe a // ConcurrentModificationException happened diff --git a/pmd-cpp/etc/grammar/Cpp.jj b/pmd-cpp/etc/grammar/Cpp.jj index 450f772c41..95640555eb 100644 --- a/pmd-cpp/etc/grammar/Cpp.jj +++ b/pmd-cpp/etc/grammar/Cpp.jj @@ -193,12 +193,14 @@ TOKEN : TOKEN: { - < #DECIMALDIGIT: ["0"-"9"] > + < #BINARYDIGIT: ["0"-"1"] > | < #OCTALDIGIT: ["0"-"7"] > +| < #DECIMALDIGIT: ["0"-"9"] > | < #HEXDIGIT: ["a"-"f", "A"-"F", "0"-"9"] > | < #INT_SUFFIX: ["u", "U", "l", "L"] | "uL" | "Ul" | "UL" | "ul" | "lu" | "Lu" | "lU" | "LU" > | < ZERO: "0" > +| < BINARY_INT_LITERAL: "0" ["b", "B"] ("'" | )+ ()? > | < OCTAL_INT_LITERAL: "0" ("'" | )+ ()? > | < DECIMAL_INT_LITERAL: ["1"-"9"] ("'" | )* ()? > | < HEXADECIMAL_INT_LITERAL: "0" ["x", "X"] ("'" | )+ ()? > diff --git a/pmd-cpp/src/test/resources/net/sourceforge/pmd/lang/cpp/cpd/testdata/literals.cpp b/pmd-cpp/src/test/resources/net/sourceforge/pmd/lang/cpp/cpd/testdata/literals.cpp index 9f0d38b06f..cbae7336ba 100644 --- a/pmd-cpp/src/test/resources/net/sourceforge/pmd/lang/cpp/cpd/testdata/literals.cpp +++ b/pmd-cpp/src/test/resources/net/sourceforge/pmd/lang/cpp/cpd/testdata/literals.cpp @@ -37,4 +37,7 @@ auto hex_literal = 0x0F00'abcd'6f3d; auto silly_example = 1'0'0'000'00; + // boolean literals + int b1 = 0B001101; // C++ 14 binary literal + int b2 = 0b000001; // C++ 14 binary literal } \ No newline at end of file diff --git a/pmd-cpp/src/test/resources/net/sourceforge/pmd/lang/cpp/cpd/testdata/literals.txt b/pmd-cpp/src/test/resources/net/sourceforge/pmd/lang/cpp/cpd/testdata/literals.txt index 3a0185425f..79afe0b26c 100644 --- a/pmd-cpp/src/test/resources/net/sourceforge/pmd/lang/cpp/cpd/testdata/literals.txt +++ b/pmd-cpp/src/test/resources/net/sourceforge/pmd/lang/cpp/cpd/testdata/literals.txt @@ -118,6 +118,18 @@ L38 [=] 24 25 [1'0'0'000'00] 26 38 [;] 38 39 -L40 +L41 + [int] 5 8 + [b1] 9 11 + [=] 12 13 + [0B001101] 14 22 + [;] 22 23 +L42 + [int] 5 8 + [b2] 9 11 + [=] 12 13 + [0b000001] 14 22 + [;] 22 23 +L43 [}] 1 2 EOF diff --git a/pmd-dist/pom.xml b/pmd-dist/pom.xml index 89038c57e5..1663f9d031 100644 --- a/pmd-dist/pom.xml +++ b/pmd-dist/pom.xml @@ -153,11 +153,6 @@ pmd-jsp ${project.version} - - net.sourceforge.pmd - pmd-visualforce - ${project.version} - net.sourceforge.pmd pmd-kotlin @@ -218,6 +213,11 @@ pmd-ui ${pmd-designer.version} + + net.sourceforge.pmd + pmd-visualforce + ${project.version} + net.sourceforge.pmd pmd-vm diff --git a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/RuleDocGenerator.java b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/RuleDocGenerator.java index 2b5d83c0cc..6f7c7e60ce 100644 --- a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/RuleDocGenerator.java +++ b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/RuleDocGenerator.java @@ -36,8 +36,8 @@ import org.apache.commons.text.StringEscapeUtils; import net.sourceforge.pmd.Rule; import net.sourceforge.pmd.RuleSet; +import net.sourceforge.pmd.RuleSetLoadException; import net.sourceforge.pmd.RuleSetLoader; -import net.sourceforge.pmd.RulesetLoadException; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.rule.RuleReference; import net.sourceforge.pmd.lang.rule.XPathRule; @@ -122,7 +122,7 @@ public class RuleDocGenerator { } else { LOG.fine("Ignoring ruleset " + filename); } - } catch (RulesetLoadException e) { + } catch (RuleSetLoadException e) { // ignore rulesets, we can't read LOG.log(Level.WARNING, "ruleset file " + filename + " ignored (" + e.getMessage() + ")", e); } diff --git a/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleDocGeneratorTest.java b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleDocGeneratorTest.java index 23d91a043d..23114c6e53 100644 --- a/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleDocGeneratorTest.java +++ b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/RuleDocGeneratorTest.java @@ -21,11 +21,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import net.sourceforge.pmd.RulePriority; import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RuleSetFactory; -import net.sourceforge.pmd.RuleSetNotFoundException; -import net.sourceforge.pmd.RulesetsFactoryUtils; +import net.sourceforge.pmd.RuleSetLoader; import net.sourceforge.pmd.docs.MockedFileWriter.FileEntry; public class RuleDocGeneratorTest { @@ -59,11 +56,11 @@ public class RuleDocGeneratorTest { } @Test - public void testSingleRuleset() throws RuleSetNotFoundException, IOException { + public void testSingleRuleset() throws IOException { RuleDocGenerator generator = new RuleDocGenerator(writer, root); - RuleSetFactory rsf = RulesetsFactoryUtils.createFactory(RulePriority.LOW, false, false, true); - RuleSet ruleset = rsf.createRuleSet("rulesets/ruledoctest/sample.xml"); + RuleSetLoader rsf = new RuleSetLoader().includeDeprecatedRuleReferences(true); + RuleSet ruleset = rsf.loadFromResource("rulesets/ruledoctest/sample.xml"); generator.generate(Arrays.asList(ruleset), Arrays.asList( 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 c2c082c44f..8b82683953 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 @@ -4,28 +4,23 @@ package net.sourceforge.pmd.docs; -import static org.junit.Assert.fail; +import static net.sourceforge.pmd.util.CollectionUtil.listOf; import java.nio.file.FileSystems; import java.nio.file.Path; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.apache.commons.io.FilenameUtils; import org.junit.Test; -import net.sourceforge.pmd.RuleSetFactory; -import net.sourceforge.pmd.RuleSetNotFoundException; -import net.sourceforge.pmd.RulesetsFactoryUtils; +import net.sourceforge.pmd.RuleSetLoader; public class RuleSetResolverTest { - private static List excludedRulesets = new ArrayList<>(); - - static { - excludedRulesets.add(FilenameUtils.normalize("pmd-test/src/main/resources/rulesets/dummy/basic.xml")); - } + private static final List EXCLUDED_RULESETS = listOf( + FilenameUtils.normalize("pmd-test/src/main/resources/rulesets/dummy/basic.xml") + ); @Test public void resolveAllRulesets() { @@ -34,13 +29,8 @@ public class RuleSetResolverTest { filterRuleSets(additionalRulesets); - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); for (String filename : additionalRulesets) { - try { - ruleSetFactory.createRuleSet(filename); - } catch (RuntimeException | RuleSetNotFoundException e) { - fail("Couldn't load ruleset " + filename + ": " + e.getMessage()); - } + new RuleSetLoader().loadFromResource(filename); // will throw if invalid } } @@ -48,7 +38,7 @@ public class RuleSetResolverTest { Iterator it = additionalRulesets.iterator(); while (it.hasNext()) { String filename = it.next(); - for (String exclusion : excludedRulesets) { + for (String exclusion : EXCLUDED_RULESETS) { if (filename.endsWith(exclusion)) { it.remove(); break; diff --git a/pmd-doc/src/test/java/net/sourceforge/pmd/docs/SidebarGeneratorTest.java b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/SidebarGeneratorTest.java index ea01604b3c..97e562e49b 100644 --- a/pmd-doc/src/test/java/net/sourceforge/pmd/docs/SidebarGeneratorTest.java +++ b/pmd-doc/src/test/java/net/sourceforge/pmd/docs/SidebarGeneratorTest.java @@ -25,7 +25,6 @@ import org.yaml.snakeyaml.DumperOptions.LineBreak; import org.yaml.snakeyaml.Yaml; import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RulesetsFactoryUtils; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageRegistry; @@ -40,8 +39,8 @@ public class SidebarGeneratorTest { @Test public void testSidebar() throws IOException { Map> rulesets = new HashMap<>(); - RuleSet ruleSet1 = RulesetsFactoryUtils.defaultFactory().createNewRuleSet("test", "test", "bestpractices.xml", Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); - RuleSet ruleSet2 = RulesetsFactoryUtils.defaultFactory().createNewRuleSet("test2", "test", "codestyle.xml", Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + RuleSet ruleSet1 = RuleSet.create("test", "test", "bestpractices.xml", Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + RuleSet ruleSet2 = RuleSet.create("test2", "test", "codestyle.xml", Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); rulesets.put(LanguageRegistry.findLanguageByTerseName("java"), Arrays.asList(ruleSet1, ruleSet2)); rulesets.put(LanguageRegistry.findLanguageByTerseName("ecmascript"), Arrays.asList(ruleSet1)); diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/Lub.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/Lub.java index 4f6ef77b77..e39c71baf6 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/Lub.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/Lub.java @@ -4,11 +4,6 @@ package net.sourceforge.pmd.lang.java.types; -import static net.sourceforge.pmd.lang.java.types.TypeOps.typeArgContains; -import static net.sourceforge.pmd.util.OptionalBool.NO; -import static net.sourceforge.pmd.util.OptionalBool.UNKNOWN; -import static net.sourceforge.pmd.util.OptionalBool.YES; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -22,14 +17,9 @@ import java.util.Set; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import org.pcollections.ConsPStack; -import org.pcollections.HashTreePSet; -import org.pcollections.PSet; -import org.pcollections.PStack; import net.sourceforge.pmd.lang.java.types.internal.infer.InferenceVar; import net.sourceforge.pmd.util.CollectionUtil; -import net.sourceforge.pmd.util.OptionalBool; /** * Helper class for {@link TypeSystem#lub(Collection)} and {@link TypeSystem#glb(Collection)}. @@ -70,10 +60,10 @@ final class Lub { /** * The "relevant" parameterizations of G, Relevant(G), is: * - *

+     * 
{@code
      * Relevant(G) = { V | 1 ≤ i ≤ k: V in ST(Ui) and V = G<...> }
      *             = { V ∈ stunion | V = G<...> }
-     * 
+ * }
* *

G must be erased (raw). * @@ -221,12 +211,6 @@ final class Lub { */ private JTypeMirror lcta(JTypeMirror t, JTypeMirror s) { - if (typeArgContains(t, s).somehow()) { - return t; - } else if (typeArgContains(s, t).somehow()) { - return s; - } - TypePair pair = new TypePair(t, s); if (lubCache.add(pair)) { @@ -336,60 +320,40 @@ final class Lub { List bounds = new ArrayList<>(mostSpecific); - JTypeMirror ck = null; // Ck is the primary bound - int primaryIdx = 0; + JTypeMirror primaryBound = null; - OptionalBool retryWithCaptureBounds = NO; - PSet cvarLowers = HashTreePSet.empty(); - PStack cvarsToRemove = ConsPStack.empty(); - JTypeMirror lastBadClass = null; for (int i = 0; i < bounds.size(); i++) { JTypeMirror ci = bounds.get(i); if (isExclusiveIntersectionBound(ci)) { // either Ci is an array, or Ci is a class, or Ci is a type var (possibly captured) // Ci is not unresolved - if (ck == null) { - ck = ci; - primaryIdx = i; + if (primaryBound == null) { + primaryBound = ci; + // move primary bound first + Collections.swap(bounds, 0, i); } else { - JTypeMirror lower = cvarLowerBound(ci); - if (lower != ci && lower != ts.NULL_TYPE) { // NOPMD CompareObjectsWithEquals - cvarLowers = cvarLowers.plus(lower); - cvarsToRemove = cvarsToRemove.plus(ci); - retryWithCaptureBounds = YES; - } else { - retryWithCaptureBounds = retryWithCaptureBounds == YES ? YES - : UNKNOWN; - } - lastBadClass = ci; + throw new IllegalArgumentException( + "Bad intersection, unrelated class types " + ci + " and " + primaryBound + " in " + types + ); } } } - switch (retryWithCaptureBounds) { // when several capture variables were found - case YES: - bounds.removeAll(cvarsToRemove); - bounds.addAll(cvarLowers); - return glb(ts, bounds); - case NO: - break; - default: - throw new IllegalArgumentException( - "Bad intersection, unrelated class types " + lastBadClass + " and " + ck + " in " + types); - } - - if (ck == null) { + if (primaryBound == null) { if (bounds.size() == 1) { return bounds.get(0); } - ck = ts.OBJECT; - } else { - // move primary bound first - Collections.swap(bounds, 0, primaryIdx); + primaryBound = ts.OBJECT; } - return new JIntersectionType(ts, ck, bounds); + return new JIntersectionType(ts, primaryBound, bounds); + } + + private static void checkGlbComponent(Collection types, JTypeMirror ci) { + if (ci.isPrimitive() || ci instanceof JWildcardType || ci instanceof JIntersectionType) { + throw new IllegalArgumentException("Bad intersection type component: " + ci + " in " + types); + } } private static void checkGlbComponent(Collection types, JTypeMirror ci) { @@ -399,7 +363,7 @@ final class Lub { } private static @NonNull List flattenRemoveTrivialBound(Collection types) { - ArrayList bounds = new ArrayList<>(types.size()); + List bounds = new ArrayList<>(types.size()); for (JTypeMirror type : types) { // flatten intersections: (A & (B & C)) => (A & B & C) @@ -423,12 +387,5 @@ final class Lub { && (ci.getSymbol() == null || !ci.getSymbol().isUnresolved()); } - private static JTypeMirror cvarLowerBound(JTypeMirror t) { - if (t instanceof JTypeVar && ((JTypeVar) t).isCaptured()) { - return ((JTypeVar) t).getLowerBound(); - } - return t; - } - } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/MapFunction.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/MapFunction.java index 79b78eb95f..cd85884be7 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/MapFunction.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/MapFunction.java @@ -9,14 +9,11 @@ import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; - /** - * A partial function built on a map + * A partial function built on a map. */ -class MapFunction implements Function { +abstract class MapFunction implements Function { private final Map map; @@ -24,7 +21,7 @@ class MapFunction implements Function { this.map = map; } - public Map getMap() { + protected Map getMap() { return map; } @@ -32,11 +29,6 @@ class MapFunction implements Function { return map.isEmpty(); } - @Override - public @Nullable R apply(@NonNull T var) { - return map.get(var); - } - @Override public String toString() { return map.entrySet().stream() diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeOps.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeOps.java index 9a9b709e89..3bceea2880 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeOps.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeOps.java @@ -434,7 +434,7 @@ public final class TypeOps { * implies convertibility (the conversion is technically called * "widening reference conversion"). You can check those cases using: * - * {@link #bySubtyping() t.isConvertibleTo(s).naturally()} + * {@link #bySubtyping() t.isConvertibleTo(s).bySubtyping()} * *

Unchecked conversion may go backwards from subtyping. For example, * {@code List} is a subtype of the raw type {@code List}, and @@ -475,8 +475,8 @@ public final class TypeOps { * {@code T <: |S|} and {@code T For example, {@code List} is a subtype of the raw * {@code Collection}, not a subtype of {@code Collection}, diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeVarImpl.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeVarImpl.java index 979352d446..3eb68145b9 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeVarImpl.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeVarImpl.java @@ -143,8 +143,7 @@ abstract class TypeVarImpl implements JTypeVar { private static final int PRIME = 997; // largest prime less than 1000 - private final JWildcardType wildcard; - private final int captureId = hashCode() % PRIME; + private final @NonNull JWildcardType wildcard; private JTypeMirror upperBound; private JTypeMirror lowerBound; @@ -160,10 +159,6 @@ abstract class TypeVarImpl implements JTypeVar { this.wildcard = wild; } - public JWildcardType getWildcard() { - return wildcard; - } - void setUpperBound(@NonNull JTypeMirror upperBound) { this.upperBound = upperBound; } @@ -195,7 +190,7 @@ abstract class TypeVarImpl implements JTypeVar { } @Override - public @Nullable JWildcardType getCapturedOrigin() { + public JWildcardType getCapturedOrigin() { return wildcard; } @@ -232,7 +227,7 @@ abstract class TypeVarImpl implements JTypeVar { @Override public @NonNull String getName() { - return "capture#" + captureId + " of " + wildcard; + return "capture#" + hashCode() % PRIME + " of " + wildcard; } } } diff --git a/pmd-java/src/main/resources/rulesets/java/design.xml b/pmd-java/src/main/resources/rulesets/java/design.xml index bef304b2e6..9ac4ae2fda 100644 --- a/pmd-java/src/main/resources/rulesets/java/design.xml +++ b/pmd-java/src/main/resources/rulesets/java/design.xml @@ -62,8 +62,6 @@ are suggested. - - diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java index c3ba3904f0..4f8ed3223a 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java @@ -14,8 +14,8 @@ import org.junit.Test; public class RuleSetFactoryTest extends AbstractRuleSetFactoryTest { @Test - public void testExclusionOfUselessParantheses() throws RuleSetNotFoundException { - RuleSetReferenceId ref = createRuleSetReferenceId( + public void testExclusionOfUselessParantheses() { + RuleSet ruleset = new RuleSetLoader().loadFromString("", "\n" + "Custom ruleset for tests\n" + " \n" + " \n" + " \n" + "\n"); - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); - RuleSet ruleset = ruleSetFactory.createRuleSet(ref); Rule rule = ruleset.getRuleByName("UselessParentheses"); assertNull(rule); } diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/cli/CLITest.java b/pmd-java/src/test/java/net/sourceforge/pmd/cli/CLITest.java index 3e82902345..2fc217381d 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/cli/CLITest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/cli/CLITest.java @@ -7,7 +7,6 @@ package net.sourceforge.pmd.cli; import static org.junit.Assert.assertTrue; import java.io.File; -import java.io.IOException; import java.util.regex.Pattern; import org.junit.Assert; @@ -39,7 +38,7 @@ public class CLITest extends BaseCLITest { } @Test - public void changeJavaVersion() throws IOException { + public void changeJavaVersion() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml", "-version", "1.5", "-language", "java", "-debug", }; String resultFilename = runTest(args, "chgJavaVersion"); @@ -54,14 +53,14 @@ public class CLITest extends BaseCLITest { } @Test - public void exitStatusWithViolations() throws IOException { + public void exitStatusWithViolations() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/errorprone.xml", }; String resultFilename = runTest(args, "exitStatusWithViolations", 4); assertTrue(FileUtil.findPatternInFile(new File(resultFilename), "Avoid empty if")); } @Test - public void exitStatusWithViolationsAndWithoutFailOnViolations() throws IOException { + public void exitStatusWithViolationsAndWithoutFailOnViolations() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/errorprone.xml", "-failOnViolation", "false", }; String resultFilename = runTest(args, "exitStatusWithViolationsAndWithoutFailOnViolations", 0); assertTrue(FileUtil.findPatternInFile(new File(resultFilename), "Avoid empty if")); @@ -71,7 +70,7 @@ public class CLITest extends BaseCLITest { * See https://sourceforge.net/p/pmd/bugs/1231/ */ @Test - public void testWrongRuleset() throws Exception { + public void testWrongRuleset() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/designn.xml", }; String filename = TEST_OUPUT_DIRECTORY + "testWrongRuleset.txt"; createTestOutputFile(filename); @@ -85,7 +84,7 @@ public class CLITest extends BaseCLITest { * See https://sourceforge.net/p/pmd/bugs/1231/ */ @Test - public void testWrongRulesetWithRulename() throws Exception { + public void testWrongRulesetWithRulename() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/designn.xml/UseCollectionIsEmpty", }; String filename = TEST_OUPUT_DIRECTORY + "testWrongRuleset.txt"; createTestOutputFile(filename); @@ -99,7 +98,7 @@ public class CLITest extends BaseCLITest { * See https://sourceforge.net/p/pmd/bugs/1231/ */ @Test - public void testWrongRulename() throws Exception { + public void testWrongRulename() { String[] args = { "-d", SOURCE_FOLDER, "-f", "text", "-R", "category/java/design.xml/ThisRuleDoesNotExist", }; String filename = TEST_OUPUT_DIRECTORY + "testWrongRuleset.txt"; createTestOutputFile(filename); diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/PMD5RulesetTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/PMD5RulesetTest.java index 23de5ce3de..9712174595 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/PMD5RulesetTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/PMD5RulesetTest.java @@ -8,15 +8,13 @@ import org.junit.Assert; import org.junit.Test; import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RuleSetFactory; import net.sourceforge.pmd.RuleSetLoader; public class PMD5RulesetTest { @Test - public void loadRuleset() throws Exception { - RuleSetFactory ruleSetFactory = new RuleSetLoader().toFactory(); - RuleSet ruleset = ruleSetFactory.createRuleSet("net/sourceforge/pmd/lang/java/pmd5ruleset.xml"); + public void loadRuleset() { + RuleSet ruleset = new RuleSetLoader().loadFromResource("net/sourceforge/pmd/lang/java/pmd5ruleset.xml"); Assert.assertNotNull(ruleset); Assert.assertNull(ruleset.getRuleByName("GuardLogStatementJavaUtil")); Assert.assertNull(ruleset.getRuleByName("GuardLogStatement")); 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 16c3142ab9..c72a41b24e 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 @@ -15,9 +15,7 @@ import org.junit.Test; import org.junit.contrib.java.lang.system.SystemErrRule; import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RuleSetFactory; import net.sourceforge.pmd.RuleSetLoader; -import net.sourceforge.pmd.RuleSetNotFoundException; public class QuickstartRulesetTest { @@ -26,15 +24,15 @@ public class QuickstartRulesetTest { @After public void cleanup() { - Handler[] handlers = Logger.getLogger(RuleSetFactory.class.getName()).getHandlers(); + Handler[] handlers = Logger.getLogger(RuleSetLoader.class.getName()).getHandlers(); for (Handler handler : handlers) { - Logger.getLogger(RuleSetFactory.class.getName()).removeHandler(handler); + Logger.getLogger(RuleSetLoader.class.getName()).removeHandler(handler); } } @Test - public void noDeprecations() throws RuleSetNotFoundException { - Logger.getLogger(RuleSetFactory.class.getName()).addHandler(new Handler() { + public void noDeprecations() { + Logger.getLogger(RuleSetLoader.class.getName()).addHandler(new Handler() { @Override public void publish(LogRecord record) { Assert.fail("No Logging expected: " + record.getMessage()); @@ -49,8 +47,8 @@ public class QuickstartRulesetTest { } }); - RuleSetFactory ruleSetFactory = new RuleSetLoader().enableCompatibility(false).toFactory(); - RuleSet quickstart = ruleSetFactory.createRuleSet("rulesets/java/quickstart.xml"); + RuleSetLoader ruleSetFactory = new RuleSetLoader().enableCompatibility(false); + RuleSet quickstart = ruleSetFactory.loadFromResource("rulesets/java/quickstart.xml"); Assert.assertFalse(quickstart.getRules().isEmpty()); } } diff --git a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/GlbTest.kt b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/GlbTest.kt index ddc3c51b35..5f8b134f08 100644 --- a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/GlbTest.kt +++ b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/GlbTest.kt @@ -7,17 +7,11 @@ package net.sourceforge.pmd.lang.java.types import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.shouldBe -import io.kotest.property.PropTestConfig import io.kotest.property.checkAll import io.kotest.property.forAll -import io.mockk.InternalPlatformDsl.toArray import net.sourceforge.pmd.lang.ast.test.shouldBeA -import net.sourceforge.pmd.lang.java.types.testdata.LubTestData -import net.sourceforge.pmd.lang.java.types.testdata.LubTestData.* -import java.io.Serializable /** * Tests "the greatest lower bound" (glb). @@ -77,6 +71,14 @@ class GlbTest : FunSpec({ } + + test("Test lub of zero types") { + shouldThrow { + ts.glb(emptyList()) + } + } + + test("Test GLB errors") { shouldThrow { diff --git a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/LubTest.kt b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/LubTest.kt index af1c0a62fe..c863a5079c 100644 --- a/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/LubTest.kt +++ b/pmd-java/src/test/kotlin/net/sourceforge/pmd/lang/java/types/LubTest.kt @@ -4,14 +4,11 @@ package net.sourceforge.pmd.lang.java.types +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.shouldBe import io.kotest.property.checkAll -import net.sourceforge.pmd.lang.java.types.testdata.LubTestData import net.sourceforge.pmd.lang.java.types.testdata.LubTestData.* -import java.io.Serializable /** * Tests "least upper bound" (lub). @@ -83,10 +80,26 @@ class LubTest : FunSpec({ ) shouldBe listOf(`t_List{Integer}`, `t_List{String}`) } - test("Test lub with related type arguments") { + test("Test lub with related type arguments (LCTA)") { + + fun checkLcta(vararg components: JTypeMirror, expected: JTypeMirror) { + val l = ts.lub(components.map { GenericSub::class[it] }) as JClassType + l.typeArgs[0] shouldBe expected + } - lub(GenericSub::class[t_Integer], GenericSub::class[t_Number]) shouldBe GenericSub::class[`?` extends t_Number] lub(GenericSub::class[t_Integer], GenericSub::class[`?` extends t_Number]) shouldBe GenericSub::class[`?` extends t_Number] + + checkLcta(t_Integer, t_Number, expected = `?` extends t_Number) + checkLcta(t_Integer, `?` extends t_Number, expected = `?` extends t_Number) + checkLcta(t_Integer, `?` `super` t_Number, expected = `?` `super` t_Integer) + checkLcta(t_Integer, `?` `super` t_Integer, expected = `?` `super` t_Integer) + checkLcta(t_Integer, `?` extends t_Integer, expected = `?` extends t_Integer) + + checkLcta(`?` `super` t_Integer, `?` `super` t_Number, expected = `?` `super` t_Integer) + checkLcta(`?` extends t_Integer, `?` extends t_Number, expected = `?` extends t_Number) + checkLcta(`?` extends t_Integer, `?` `super` t_Number, expected = `?`) + checkLcta(`?` `super` t_Integer, `?` extends t_Number, expected = `?`) + } test("Test lub with identical type arguments") { @@ -109,6 +122,12 @@ class LubTest : FunSpec({ } + test("Test lub of zero types") { + shouldThrow { + ts.lub(emptyList()) + } + } + test("Test lub with interface intersection") { // this example recurses into lub(Comparable, Comparable), at which point diff --git a/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt index 0bccf36eec..0fa6ec2b80 100644 --- a/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt +++ b/pmd-lang-test/src/main/kotlin/net/sourceforge/pmd/lang/ast/test/BaseParsingHelper.kt @@ -218,7 +218,7 @@ abstract class BaseParsingHelper, T : RootNode ctx.report = report ctx.sourceCodeFile = File(filename) ctx.isIgnoreExceptions = false - val rules = RuleSetFactory().createSingleRuleRuleSet(rule) + val rules = RuleSet.forSingleRule(rule) try { p.sourceCodeProcessor.processSourceCode(StringReader(code), RuleSets(rules), ctx) } catch (e: PMDException) { diff --git a/pmd-scala-modules/pmd-scala-common/src/test/java/net/sourceforge/pmd/lang/scala/ast/ScalaParsingHelper.java b/pmd-scala-modules/pmd-scala-common/src/test/java/net/sourceforge/pmd/lang/scala/ast/ScalaParsingHelper.java index 111815dbca..7c4e8d4978 100644 --- a/pmd-scala-modules/pmd-scala-common/src/test/java/net/sourceforge/pmd/lang/scala/ast/ScalaParsingHelper.java +++ b/pmd-scala-modules/pmd-scala-common/src/test/java/net/sourceforge/pmd/lang/scala/ast/ScalaParsingHelper.java @@ -20,5 +20,4 @@ public final class ScalaParsingHelper extends BaseParsingHelper @@ -101,13 +100,15 @@ public class AbstractLanguageVersionTest { return; } - ResourceLoader rl = new ResourceLoader(); Properties props = new Properties(); - String rulesetsProperties = "category/" + simpleTerseName + "/categories.properties"; - try (InputStream inputStream = rl.loadClassPathResourceAsStreamOrThrow(rulesetsProperties)) { + String rulesetsProperties = "/category/" + simpleTerseName + "/categories.properties"; + try (InputStream inputStream = getClass().getResourceAsStream(rulesetsProperties)) { + if (inputStream == null) { + throw new IOException(); + } props.load(inputStream); } - assertRulesetsAndCategoriesProperties(rl, props); + assertRulesetsAndCategoriesProperties(props); } /** @@ -123,16 +124,15 @@ public class AbstractLanguageVersionTest { return; } - ResourceLoader rl = new ResourceLoader(); Properties props = new Properties(); - String rulesetsProperties = "rulesets/" + simpleTerseName + "/rulesets.properties"; - InputStream inputStream = rl.loadClassPathResourceAsStream(rulesetsProperties); + String rulesetsProperties = "/rulesets/" + simpleTerseName + "/rulesets.properties"; + InputStream inputStream = getClass().getResourceAsStream(rulesetsProperties); if (inputStream != null) { // rulesets.properties file exists try (InputStream in = inputStream) { props.load(in); } - assertRulesetsAndCategoriesProperties(rl, props); + assertRulesetsAndCategoriesProperties(props); } } @@ -155,12 +155,11 @@ public class AbstractLanguageVersionTest { + " in the language versions of its language", 1, count); } - private void assertRulesetsAndCategoriesProperties(ResourceLoader rl, Properties props) - throws IOException, RuleSetNotFoundException { + private void assertRulesetsAndCategoriesProperties(Properties props) throws IOException { String rulesetFilenames = props.getProperty("rulesets.filenames"); assertNotNull(rulesetFilenames); - RuleSetFactory factory = RulesetsFactoryUtils.defaultFactory(); + RuleSetLoader factory = new RuleSetLoader(); if (rulesetFilenames.trim().isEmpty()) { return; @@ -168,10 +167,7 @@ public class AbstractLanguageVersionTest { String[] rulesets = rulesetFilenames.split(","); for (String r : rulesets) { - try (InputStream stream = rl.loadClassPathResourceAsStream(r)) { - assertNotNull(stream); - } - RuleSet ruleset = factory.createRuleSet(r); + RuleSet ruleset = factory.loadFromResource(r); assertNotNull(ruleset); } } diff --git a/pmd-test/src/main/java/net/sourceforge/pmd/AbstractRuleSetFactoryTest.java b/pmd-test/src/main/java/net/sourceforge/pmd/AbstractRuleSetFactoryTest.java index 0394a990a4..b262bbcf46 100644 --- a/pmd-test/src/main/java/net/sourceforge/pmd/AbstractRuleSetFactoryTest.java +++ b/pmd-test/src/main/java/net/sourceforge/pmd/AbstractRuleSetFactoryTest.java @@ -12,11 +12,13 @@ import static org.junit.Assert.fail; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -26,7 +28,6 @@ import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; import java.util.regex.Pattern; -import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; @@ -44,7 +45,6 @@ import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.rule.RuleReference; import net.sourceforge.pmd.lang.rule.XPathRule; import net.sourceforge.pmd.properties.PropertyDescriptor; -import net.sourceforge.pmd.util.ResourceLoader; /** * Base test class to verify the language's rulesets. This class should be @@ -54,13 +54,24 @@ public abstract class AbstractRuleSetFactoryTest { @org.junit.Rule public final SystemErrRule systemErrRule = new SystemErrRule().enableLog().muteForSuccessfulTests(); - private static SAXParserFactory saxParserFactory; private static ValidateDefaultHandler validateDefaultHandler; private static SAXParser saxParser; protected Set validXPathClassNames = new HashSet<>(); + private final Set languagesToSkip = new HashSet<>(); public AbstractRuleSetFactoryTest() { + this(new String[0]); + } + + /** + * Constructor used when a module that depends on another module wants to filter out the dependee's rulesets. + * + * @param languagesToSkip {@link Language}s terse names that appear in the classpath via a dependency, but should be + * skipped because they aren't the primary language which the concrete instance of this class is testing. + */ + public AbstractRuleSetFactoryTest(String... languagesToSkip) { + this.languagesToSkip.addAll(Arrays.asList(languagesToSkip)); validXPathClassNames.add(XPathRule.class.getName()); } @@ -72,7 +83,7 @@ public abstract class AbstractRuleSetFactoryTest { */ @BeforeClass public static void init() throws Exception { - saxParserFactory = SAXParserFactory.newInstance(); + SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); saxParserFactory.setValidating(true); saxParserFactory.setNamespaceAware(true); @@ -259,43 +270,47 @@ public abstract class AbstractRuleSetFactoryTest { } // Gets all test PMD Ruleset XML files - private List getRuleSetFileNames() throws IOException, RuleSetNotFoundException { + private List getRuleSetFileNames() throws IOException { List result = new ArrayList<>(); for (Language language : LanguageRegistry.getLanguages()) { + if (this.languagesToSkip.contains(language.getTerseName())) { + continue; + } result.addAll(getRuleSetFileNames(language.getTerseName())); } return result; } - private List getRuleSetFileNames(String language) throws IOException, RuleSetNotFoundException { + private List getRuleSetFileNames(String language) throws IOException { List ruleSetFileNames = new ArrayList<>(); - try { - Properties properties = new Properties(); - try (InputStream is = new ResourceLoader().loadClassPathResourceAsStreamOrThrow("rulesets/" + language + "/rulesets.properties")) { - properties.load(is); - } - String fileNames = properties.getProperty("rulesets.filenames"); - StringTokenizer st = new StringTokenizer(fileNames, ","); - while (st.hasMoreTokens()) { - ruleSetFileNames.add(st.nextToken()); - } - } catch (RuleSetNotFoundException e) { + Properties properties = new Properties(); + @SuppressWarnings("PMD.CloseResource") + InputStream input = getClass().getResourceAsStream("rulesets/" + language + "/rulesets.properties"); + if (input == null) { // this might happen if a language is only support by CPD, but not // by PMD System.err.println("No ruleset found for language " + language); + return Collections.emptyList(); } + try (InputStream is = input) { + properties.load(is); + } + String fileNames = properties.getProperty("rulesets.filenames"); + StringTokenizer st = new StringTokenizer(fileNames, ","); + while (st.hasMoreTokens()) { + ruleSetFileNames.add(st.nextToken()); + } + return ruleSetFileNames; } - private RuleSet loadRuleSetByFileName(String ruleSetFileName) throws RuleSetNotFoundException { - RuleSetFactory rsf = RulesetsFactoryUtils.defaultFactory(); - return rsf.createRuleSet(ruleSetFileName); + private RuleSet loadRuleSetByFileName(String ruleSetFileName) { + return new RuleSetLoader().loadFromResource(ruleSetFileName); } - private boolean validateAgainstSchema(String fileName) - throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException { + private boolean validateAgainstSchema(String fileName) throws IOException, SAXException { try (InputStream inputStream = loadResourceAsStream(fileName)) { boolean valid = validateAgainstSchema(inputStream); if (!valid) { @@ -305,16 +320,14 @@ public abstract class AbstractRuleSetFactoryTest { } } - private boolean validateAgainstSchema(InputStream inputStream) - throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException { + private boolean validateAgainstSchema(InputStream inputStream) throws IOException, SAXException { saxParser.parse(inputStream, validateDefaultHandler.resetValid()); inputStream.close(); return validateDefaultHandler.isValid(); } - private boolean validateAgainstDtd(String fileName) - throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException { + private boolean validateAgainstDtd(String fileName) throws IOException, SAXException { try (InputStream inputStream = loadResourceAsStream(fileName)) { boolean valid = validateAgainstDtd(inputStream); if (!valid) { @@ -324,8 +337,7 @@ public abstract class AbstractRuleSetFactoryTest { } } - private boolean validateAgainstDtd(InputStream inputStream) - throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException { + private boolean validateAgainstDtd(InputStream inputStream) throws IOException, SAXException { // Read file into memory String file = readFullyToString(inputStream); @@ -365,12 +377,11 @@ public abstract class AbstractRuleSetFactoryTest { } } - private static InputStream loadResourceAsStream(String resource) throws RuleSetNotFoundException { - return new ResourceLoader().loadClassPathResourceAsStreamOrThrow(resource); + private InputStream loadResourceAsStream(String resource) { + return getClass().getResourceAsStream(resource); } - private void testRuleSet(String fileName) - throws IOException, RuleSetNotFoundException, ParserConfigurationException, SAXException { + private void testRuleSet(String fileName) throws IOException, SAXException { // Load original XML // String xml1 = @@ -389,8 +400,8 @@ public abstract class AbstractRuleSetFactoryTest { // System.out.println("xml2: " + xml2); // Read RuleSet from XML, first time - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); - RuleSet ruleSet2 = ruleSetFactory.createRuleSet(createRuleSetReferenceId(xml2)); + RuleSetLoader loader = new RuleSetLoader(); + RuleSet ruleSet2 = loader.loadFromString("", xml2); // Do write/read a 2nd time, just to be sure @@ -403,7 +414,7 @@ public abstract class AbstractRuleSetFactoryTest { // System.out.println("xml3: " + xml3); // Read RuleSet from XML, second time - RuleSet ruleSet3 = ruleSetFactory.createRuleSet(createRuleSetReferenceId(xml3)); + RuleSet ruleSet3 = loader.loadFromString("", xml3); // The 2 written XMLs should all be valid w.r.t Schema/DTD assertTrue("1st roundtrip RuleSet XML is not valid against Schema (filename: " + fileName + ")", @@ -430,10 +441,10 @@ public abstract class AbstractRuleSetFactoryTest { private void assertEqualsRuleSet(String message, RuleSet ruleSet1, RuleSet ruleSet2) { assertEquals(message + ", RuleSet name", ruleSet1.getName(), ruleSet2.getName()); assertEquals(message + ", RuleSet description", ruleSet1.getDescription(), ruleSet2.getDescription()); - assertEquals(message + ", RuleSet exclude patterns", ruleSet1.getExcludePatterns(), - ruleSet2.getExcludePatterns()); - assertEquals(message + ", RuleSet include patterns", ruleSet1.getIncludePatterns(), - ruleSet2.getIncludePatterns()); + assertEquals(message + ", RuleSet exclude patterns", ruleSet1.getFileExclusions(), + ruleSet2.getFileExclusions()); + assertEquals(message + ", RuleSet include patterns", ruleSet1.getFileInclusions(), + ruleSet2.getFileInclusions()); assertEquals(message + ", RuleSet rule count", ruleSet1.getRules().size(), ruleSet2.getRules().size()); for (int i = 0; i < ruleSet1.getRules().size(); i++) { @@ -495,22 +506,6 @@ public abstract class AbstractRuleSetFactoryTest { } } - /** - * Create a {@link RuleSetReferenceId} by the given XML string. - * - * @param ruleSetXml - * the ruleset file content as string - * @return the {@link RuleSetReferenceId} - */ - protected static RuleSetReferenceId createRuleSetReferenceId(final String ruleSetXml) { - return new RuleSetReferenceId(null) { - @Override - public InputStream getInputStream(ResourceLoader resourceLoader) throws RuleSetNotFoundException { - return new ByteArrayInputStream(ruleSetXml.getBytes(StandardCharsets.UTF_8)); - } - }; - } - /** * Validator for the SAX parser */ @@ -534,17 +529,17 @@ public abstract class AbstractRuleSetFactoryTest { } @Override - public void error(SAXParseException e) throws SAXException { + public void error(SAXParseException e) { log("Error", e); } @Override - public void fatalError(SAXParseException e) throws SAXException { + public void fatalError(SAXParseException e) { log("FatalError", e); } @Override - public void warning(SAXParseException e) throws SAXException { + public void warning(SAXParseException e) { log("Warning", e); } @@ -555,17 +550,15 @@ public abstract class AbstractRuleSetFactoryTest { } @Override - public InputSource resolveEntity(String publicId, String systemId) throws IOException, SAXException { + public InputSource resolveEntity(String publicId, String systemId) throws IOException { String resource = schemaMapping.get(systemId); if (resource != null) { - try { - InputStream inputStream = loadResourceAsStream(resource); - return new InputSource(inputStream); - } catch (RuleSetNotFoundException e) { - System.err.println(e.getMessage()); - throw new IOException(e.getMessage()); + InputStream inputStream = getClass().getResourceAsStream(resource); + if (inputStream == null) { + throw new FileNotFoundException(resource); } + return new InputSource(inputStream); } throw new IllegalArgumentException( "No clue how to handle: publicId=" + publicId + ", systemId=" + systemId); diff --git a/pmd-test/src/main/java/net/sourceforge/pmd/lang/ParserOptionsTest.java b/pmd-test/src/main/java/net/sourceforge/pmd/lang/ParserOptionsTest.java index 2027fa82b3..131c369b1e 100644 --- a/pmd-test/src/main/java/net/sourceforge/pmd/lang/ParserOptionsTest.java +++ b/pmd-test/src/main/java/net/sourceforge/pmd/lang/ParserOptionsTest.java @@ -9,7 +9,9 @@ import org.junit.Test; /** * Unit tests for {@link ParserOptions}. + * @deprecated for removal in PMD 7.0. Use {@link ParserOptionsTestUtils}. */ +@Deprecated public class ParserOptionsTest { /** @@ -42,42 +44,7 @@ public class ParserOptionsTest { * fourth option instance - equals second */ public static void verifyOptionsEqualsHashcode(ParserOptions options1, ParserOptions options2, - ParserOptions options3, ParserOptions options4) { - // Objects should be different - Assert.assertNotSame(options1, options2); - Assert.assertNotSame(options1, options2); - Assert.assertNotSame(options1, options3); - Assert.assertNotSame(options2, options3); - Assert.assertNotSame(options2, options4); - Assert.assertNotSame(options3, options4); - - // Check all 16 equality combinations - Assert.assertEquals(options1, options1); - Assert.assertFalse(options1.equals(options2)); - Assert.assertEquals(options1, options3); - Assert.assertFalse(options1.equals(options4)); - - Assert.assertFalse(options2.equals(options1)); - Assert.assertEquals(options2, options2); - Assert.assertFalse(options2.equals(options3)); - Assert.assertEquals(options2, options4); - - Assert.assertEquals(options3, options1); - Assert.assertFalse(options3.equals(options2)); - Assert.assertEquals(options3, options3); - Assert.assertFalse(options3.equals(options4)); - - Assert.assertFalse(options4.equals(options1)); - Assert.assertEquals(options4, options2); - Assert.assertFalse(options4.equals(options3)); - Assert.assertEquals(options4, options4); - - // Hashcodes should match up - Assert.assertNotEquals(options1.hashCode(), options2.hashCode()); - Assert.assertEquals(options1.hashCode(), options3.hashCode()); - Assert.assertNotEquals(options1.hashCode(), options4.hashCode()); - Assert.assertNotEquals(options2.hashCode(), options3.hashCode()); - Assert.assertEquals(options2.hashCode(), options4.hashCode()); - Assert.assertNotEquals(options3.hashCode(), options4.hashCode()); + ParserOptions options3, ParserOptions options4) { + ParserOptionsTestUtils.verifyOptionsEqualsHashcode(options1, options2, options3, options4); } } diff --git a/pmd-test/src/main/java/net/sourceforge/pmd/lang/ParserOptionsTestUtils.java b/pmd-test/src/main/java/net/sourceforge/pmd/lang/ParserOptionsTestUtils.java new file mode 100644 index 0000000000..d8612c4051 --- /dev/null +++ b/pmd-test/src/main/java/net/sourceforge/pmd/lang/ParserOptionsTestUtils.java @@ -0,0 +1,65 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang; + +import org.junit.Assert; + +public final class ParserOptionsTestUtils { + private ParserOptionsTestUtils() { + } + + /** + * Verify equals and hashCode for 4 {@link ParserOptions} instances. The + * given options should be as follows: 1 and 3 are equals, as are 2 and 4. + * + * @param options1 + * first option instance - equals third + * @param options2 + * second option instance - equals fourth + * @param options3 + * third option instance - equals first + * @param options4 + * fourth option instance - equals second + */ + public static void verifyOptionsEqualsHashcode(ParserOptions options1, ParserOptions options2, + ParserOptions options3, ParserOptions options4) { + // Objects should be different + Assert.assertNotSame(options1, options2); + Assert.assertNotSame(options1, options2); + Assert.assertNotSame(options1, options3); + Assert.assertNotSame(options2, options3); + Assert.assertNotSame(options2, options4); + Assert.assertNotSame(options3, options4); + + // Check all 16 equality combinations + Assert.assertEquals(options1, options1); + Assert.assertNotEquals(options1, options2); + Assert.assertEquals(options1, options3); + Assert.assertNotEquals(options1, options4); + + Assert.assertNotEquals(options2, options1); + Assert.assertEquals(options2, options2); + Assert.assertNotEquals(options2, options3); + Assert.assertEquals(options2, options4); + + Assert.assertEquals(options3, options1); + Assert.assertNotEquals(options3, options2); + Assert.assertEquals(options3, options3); + Assert.assertNotEquals(options3, options4); + + Assert.assertNotEquals(options4, options1); + Assert.assertEquals(options4, options2); + Assert.assertNotEquals(options4, options3); + Assert.assertEquals(options4, options4); + + // Hashcodes should match up + Assert.assertNotEquals(options1.hashCode(), options2.hashCode()); + Assert.assertEquals(options1.hashCode(), options3.hashCode()); + Assert.assertNotEquals(options1.hashCode(), options4.hashCode()); + Assert.assertNotEquals(options2.hashCode(), options3.hashCode()); + Assert.assertEquals(options2.hashCode(), options4.hashCode()); + Assert.assertNotEquals(options3.hashCode(), options4.hashCode()); + } +} diff --git a/pmd-test/src/main/java/net/sourceforge/pmd/testframework/RuleTst.java b/pmd-test/src/main/java/net/sourceforge/pmd/testframework/RuleTst.java index b6e3188d81..8861452344 100644 --- a/pmd-test/src/main/java/net/sourceforge/pmd/testframework/RuleTst.java +++ b/pmd-test/src/main/java/net/sourceforge/pmd/testframework/RuleTst.java @@ -40,10 +40,10 @@ import net.sourceforge.pmd.Report; import net.sourceforge.pmd.Rule; import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RuleSetNotFoundException; +import net.sourceforge.pmd.RuleSetLoadException; +import net.sourceforge.pmd.RuleSetLoader; import net.sourceforge.pmd.RuleSets; import net.sourceforge.pmd.RuleViolation; -import net.sourceforge.pmd.RulesetsFactoryUtils; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.LanguageVersion; @@ -100,14 +100,14 @@ public abstract class RuleTst { */ public Rule findRule(String ruleSet, String ruleName) { try { - Rule rule = RulesetsFactoryUtils.defaultFactory().createRuleSets(ruleSet).getRuleByName(ruleName); + Rule rule = new RuleSetLoader().loadFromResource(ruleSet).getRuleByName(ruleName); if (rule == null) { fail("Rule " + ruleName + " not found in ruleset " + ruleSet); } else { rule.setRuleSetName(ruleSet); } return rule; - } catch (RuleSetNotFoundException e) { + } catch (RuleSetLoadException e) { e.printStackTrace(); fail("Couldn't find ruleset " + ruleSet); return null; diff --git a/pmd-test/src/test/java/net/sourceforge/pmd/lang/ParserOptionsUnitTest.java b/pmd-test/src/test/java/net/sourceforge/pmd/lang/ParserOptionsUnitTest.java new file mode 100644 index 0000000000..8fc99fe8d5 --- /dev/null +++ b/pmd-test/src/test/java/net/sourceforge/pmd/lang/ParserOptionsUnitTest.java @@ -0,0 +1,206 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Assert; +import org.junit.Test; + +import net.sourceforge.pmd.PMD; +import net.sourceforge.pmd.properties.PropertyDescriptor; +import net.sourceforge.pmd.properties.PropertyFactory; +import net.sourceforge.pmd.test.lang.DummyLanguageModule; + +/** + * Unit tests for {@link ParserOptions}. + * This class is located in the pmd-test project instead of pmd-core so that it can invoke + * {@link ParserOptionsTestUtils#verifyOptionsEqualsHashcode} + * + * TODO: 7.0.0: Rename to ParserOptionsTest when {@link ParserOptionsTest} is removed. + */ +public class ParserOptionsUnitTest { + private static final List DEFAULT_LIST = Arrays.asList("value1", "value2"); + private static final String DEFAULT_STRING = "value3"; + private static final List OVERRIDDEN_LIST = Arrays.asList("override1", "override2"); + private static final String OVERRIDDEN_STRING = "override3"; + + private static class TestParserOptions extends ParserOptions { + private static final PropertyDescriptor> LIST_DESCRIPTOR = + PropertyFactory.stringListProperty("listOfStringValues") + .desc("A list of values for testing.") + .defaultValue(DEFAULT_LIST) + .delim(',') + .build(); + + private static final PropertyDescriptor STRING_DESCRIPTOR = + PropertyFactory.stringProperty("stringValue") + .desc("A single value for testing.") + .defaultValue(DEFAULT_STRING) + .build(); + + private TestParserOptions() { + super(DummyLanguageModule.TERSE_NAME); + defineProperty(LIST_DESCRIPTOR); + defineProperty(STRING_DESCRIPTOR); + overridePropertiesFromEnv(); + } + } + + /** + * SuppressMarker should be initially null and changeable. + */ + @Test + public void testSuppressMarker() { + ParserOptions parserOptions = new ParserOptions(); + Assert.assertEquals(PMD.SUPPRESS_MARKER, parserOptions.getSuppressMarker()); + parserOptions.setSuppressMarker("foo"); + Assert.assertEquals("foo", parserOptions.getSuppressMarker()); + } + + @Test + public void testDefaultPropertyDescriptors() { + TestParserOptions parserOptions = new TestParserOptions(); + assertEquals(DEFAULT_LIST, parserOptions.getProperty(TestParserOptions.LIST_DESCRIPTOR)); + assertEquals(DEFAULT_STRING, parserOptions.getProperty(TestParserOptions.STRING_DESCRIPTOR)); + } + + @Test + public void testOverriddenPropertyDescriptors() { + TestParserOptions parserOptions = new TestParserOptions(); + parserOptions.setProperty(TestParserOptions.LIST_DESCRIPTOR, OVERRIDDEN_LIST); + parserOptions.setProperty(TestParserOptions.STRING_DESCRIPTOR, OVERRIDDEN_STRING); + + assertEquals(OVERRIDDEN_LIST, parserOptions.getProperty(TestParserOptions.LIST_DESCRIPTOR)); + assertEquals(OVERRIDDEN_STRING, parserOptions.getProperty(TestParserOptions.STRING_DESCRIPTOR)); + } + + @Test + public void testEnvOverriddenPropertyDescriptors() { + TestParserOptions parserOptions = new TestParserOptions() { + @Override + protected String getEnvValue(PropertyDescriptor propertyDescriptor) { + if (propertyDescriptor.equals(TestParserOptions.LIST_DESCRIPTOR)) { + return StringUtils.join(OVERRIDDEN_LIST, ","); + } else if (propertyDescriptor.equals(TestParserOptions.STRING_DESCRIPTOR)) { + return OVERRIDDEN_STRING; + } else { + throw new RuntimeException("Should not happen"); + } + } + }; + + assertEquals(OVERRIDDEN_LIST, parserOptions.getProperty(TestParserOptions.LIST_DESCRIPTOR)); + assertEquals(OVERRIDDEN_STRING, parserOptions.getProperty(TestParserOptions.STRING_DESCRIPTOR)); + } + + @Test + public void testEmptyPropertyDescriptors() { + TestParserOptions vfParserOptions = new TestParserOptions() { + @Override + protected String getEnvValue(PropertyDescriptor propertyDescriptor) { + if (propertyDescriptor.equals(TestParserOptions.LIST_DESCRIPTOR) + || propertyDescriptor.equals(TestParserOptions.STRING_DESCRIPTOR)) { + return ""; + } else { + throw new RuntimeException("Should not happen"); + } + } + }; + + assertEquals(Collections.emptyList(), vfParserOptions.getProperty(TestParserOptions.LIST_DESCRIPTOR)); + assertEquals("", vfParserOptions.getProperty(TestParserOptions.STRING_DESCRIPTOR)); + } + + /** + * Verify that the equals and hashCode methods work as expected. + * TODO: Consider using Guava's EqualsTester + */ + @Test + public void testSuppressMarkerEqualsHashCode() { + ParserOptions options1; + ParserOptions options2; + ParserOptions options3; + ParserOptions options4; + + // SuppressMarker + options1 = new ParserOptions(); + options2 = new ParserOptions(); + options3 = new ParserOptions(); + options4 = new ParserOptions(); + options1.setSuppressMarker("foo"); + options2.setSuppressMarker("bar"); + options3.setSuppressMarker("foo"); + options4.setSuppressMarker("bar"); + ParserOptionsTestUtils.verifyOptionsEqualsHashcode(options1, options2, options3, options4); + + // PropertyDescriptor + options1 = new ParserOptions(); + options2 = new ParserOptions(); + options3 = new ParserOptions(); + options4 = new ParserOptions(); + options1.defineProperty(TestParserOptions.LIST_DESCRIPTOR); + options2.defineProperty(TestParserOptions.STRING_DESCRIPTOR); + options3.defineProperty(TestParserOptions.LIST_DESCRIPTOR); + options4.defineProperty(TestParserOptions.STRING_DESCRIPTOR); + ParserOptionsTestUtils.verifyOptionsEqualsHashcode(options1, options2, options3, options4); + + // PropertyValue + options1 = new ParserOptions(); + options2 = new ParserOptions(); + options3 = new ParserOptions(); + options4 = new ParserOptions(); + options1.defineProperty(TestParserOptions.STRING_DESCRIPTOR, DEFAULT_STRING); + options2.defineProperty(TestParserOptions.STRING_DESCRIPTOR, OVERRIDDEN_STRING); + options3.defineProperty(TestParserOptions.STRING_DESCRIPTOR, DEFAULT_STRING); + options4.defineProperty(TestParserOptions.STRING_DESCRIPTOR, OVERRIDDEN_STRING); + ParserOptionsTestUtils.verifyOptionsEqualsHashcode(options1, options2, options3, options4); + + // Language + options1 = new ParserOptions(DummyLanguageModule.TERSE_NAME); + options2 = new ParserOptions(); + options3 = new ParserOptions(DummyLanguageModule.TERSE_NAME); + options4 = new ParserOptions(); + ParserOptionsTestUtils.verifyOptionsEqualsHashcode(options1, options2, options3, options4); + + // SuppressMarker, PropertyDescriptor, PropertyValue, Language + options1 = new ParserOptions(DummyLanguageModule.TERSE_NAME); + options2 = new ParserOptions(); + options3 = new ParserOptions(DummyLanguageModule.TERSE_NAME); + options4 = new ParserOptions(); + options1.setSuppressMarker("foo"); + options2.setSuppressMarker("bar"); + options3.setSuppressMarker("foo"); + options4.setSuppressMarker("bar"); + options1.defineProperty(TestParserOptions.LIST_DESCRIPTOR, DEFAULT_LIST); + options2.defineProperty(TestParserOptions.STRING_DESCRIPTOR, OVERRIDDEN_STRING); + options3.defineProperty(TestParserOptions.LIST_DESCRIPTOR, DEFAULT_LIST); + options4.defineProperty(TestParserOptions.STRING_DESCRIPTOR, OVERRIDDEN_STRING); + ParserOptionsTestUtils.verifyOptionsEqualsHashcode(options1, options2, options3, options4); + + assertFalse(options1.equals(null)); + } + + @Test + public void testGetEnvironmentVariableName() { + ParserOptions parserOptions = new TestParserOptions(); + assertEquals("PMD_DUMMY_LISTOFSTRINGVALUES", + parserOptions.getEnvironmentVariableName(TestParserOptions.LIST_DESCRIPTOR)); + assertEquals("PMD_DUMMY_STRINGVALUE", + parserOptions.getEnvironmentVariableName(TestParserOptions.STRING_DESCRIPTOR)); + } + + @Test(expected = IllegalStateException.class) + public void testGetEnvironmentVariableNameThrowsExceptionIfLanguageIsNull() { + ParserOptions parserOptions = new ParserOptions(); + parserOptions.getEnvironmentVariableName(TestParserOptions.LIST_DESCRIPTOR); + } +} diff --git a/pmd-visualforce/pom.xml b/pmd-visualforce/pom.xml index 3fdbf66647..bb3ae9a749 100644 --- a/pmd-visualforce/pom.xml +++ b/pmd-visualforce/pom.xml @@ -11,6 +11,10 @@ ../ + + 8 + + @@ -78,6 +82,12 @@ pmd-core + + net.sourceforge.pmd + pmd-apex + ${project.version} + + junit junit diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/DataType.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/DataType.java new file mode 100644 index 0000000000..d172999181 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/DataType.java @@ -0,0 +1,131 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import apex.jorje.semantic.symbol.type.BasicType; + +/** + * Represents all data types that can be referenced from a Visualforce page. This enum consolidates the data types + * available to CustomFields and Apex. It uses the naming convention of CustomFields. + * + * See https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_field_types.htm#meta_type_fieldtype + */ +public enum DataType { + AutoNumber(false), + Checkbox(false, BasicType.BOOLEAN), + Currency(false, BasicType.CURRENCY), + Date(false, BasicType.DATE), + DateTime(false, BasicType.DATE_TIME), + Email(false), + EncryptedText(true), + ExternalLookup(true), + File(false), + Hierarchy(false), + Html(false), + IndirectLookup(false), + Location(false), + LongTextArea(true), + Lookup(false, BasicType.ID), + MasterDetail(false), + MetadataRelationship(false), + MultiselectPicklist(true), + Note(true), + Number(false, BasicType.DECIMAL, BasicType.DOUBLE, BasicType.INTEGER, BasicType.LONG), + Percent(false), + Phone(false), + Picklist(true), + Summary(false), + Text(true, BasicType.STRING), + TextArea(true), + Time(false, BasicType.TIME), + Url(false), + /** + * Indicates that Metatada was found, but it's type was not mappable. This could because it is a type which isn't + * mapped, or it was an edge case where the type was ambiguously defined in the Metadata. + */ + Unknown(true); + + private static final Logger LOGGER = Logger.getLogger(DataType.class.getName()); + + + /** + * True if this field is an XSS risk + */ + public final boolean requiresEscaping; + + /** + * The set of {@link BasicType}s that map to this type. Multiple types can map to a single instance of this enum. + */ + private final Set basicTypes; + + /** + * A case insensitive map of the enum name to its instance. The case metadata is not guaranteed to have the correct + * case. + */ + private static final Map CASE_INSENSITIVE_MAP = new HashMap<>(); + + /** + * Map of BasicType to DataType. Multiple BasicTypes may map to one DataType. + */ + private static final Map BASIC_TYPE_MAP = new HashMap<>(); + + static { + for (DataType dataType : DataType.values()) { + CASE_INSENSITIVE_MAP.put(dataType.name().toLowerCase(Locale.ROOT), dataType); + for (BasicType basicType : dataType.basicTypes) { + BASIC_TYPE_MAP.put(basicType, dataType); + } + } + } + + /** + * Map to correct instance, returns {@code Unknown} if the value can't be mapped. + */ + public static DataType fromString(String value) { + value = value != null ? value : ""; + DataType dataType = CASE_INSENSITIVE_MAP.get(value.toLowerCase(Locale.ROOT)); + + if (dataType == null) { + dataType = DataType.Unknown; + LOGGER.fine("Unable to determine DataType of " + value); + } + + return dataType; + } + + /** + * Map to correct instance, returns {@code Unknown} if the value can't be mapped. + */ + public static DataType fromBasicType(BasicType value) { + DataType dataType = value != null ? BASIC_TYPE_MAP.get(value) : null; + + if (dataType == null) { + dataType = DataType.Unknown; + LOGGER.fine("Unable to determine DataType of " + value); + } + + return dataType; + } + + DataType(boolean requiresEscaping) { + this(requiresEscaping, null); + } + + DataType(boolean requiresEscaping, BasicType...basicTypes) { + this.requiresEscaping = requiresEscaping; + this.basicTypes = new HashSet<>(); + if (basicTypes != null) { + this.basicTypes.addAll(Arrays.asList(basicTypes)); + } + } +} diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfHandler.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfHandler.java index e49f653c09..34b9f1ab2c 100644 --- a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfHandler.java +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfHandler.java @@ -16,4 +16,8 @@ public class VfHandler extends AbstractPmdLanguageVersionHandler { return new VfParser(); } + @Override + public ParserOptions getDefaultParserOptions() { + return new VfParserOptions(); + } } diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfParserOptions.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfParserOptions.java new file mode 100644 index 0000000000..16ba794e88 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfParserOptions.java @@ -0,0 +1,49 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +import net.sourceforge.pmd.lang.ParserOptions; +import net.sourceforge.pmd.properties.PropertyDescriptor; +import net.sourceforge.pmd.properties.PropertyFactory; + +public class VfParserOptions extends ParserOptions { + static final List DEFAULT_APEX_DIRECTORIES = Collections.singletonList(".." + File.separator + "classes"); + static final List DEFAULT_OBJECT_DIRECTORIES = Collections.singletonList(".." + File.separator + "objects"); + + /** + * Directory that contains Apex classes that may be referenced from a Visualforce page. + * + *

Env variable is {@code PMD_VF_APEXDIRECTORIES}. + */ + public static final PropertyDescriptor> APEX_DIRECTORIES_DESCRIPTOR = + PropertyFactory.stringListProperty("apexDirectories") + .desc("Location of Apex Class directories. Absolute or relative to the Visualforce directory.") + .defaultValue(DEFAULT_APEX_DIRECTORIES) + .delim(',') + .build(); + + /** + * Directory that contains Object definitions that may be referenced from a Visualforce page. + * + *

Env variable is {@code PMD_VF_OBJECTSDIRECTORIES}. + */ + public static final PropertyDescriptor> OBJECTS_DIRECTORIES_DESCRIPTOR = + PropertyFactory.stringListProperty("objectsDirectories") + .desc("Location of Custom Object directories. Absolute or relative to the Visualforce directory.") + .defaultValue(DEFAULT_OBJECT_DIRECTORIES) + .delim(',') + .build(); + + public VfParserOptions() { + super(VfLanguageModule.TERSE_NAME); + defineProperty(APEX_DIRECTORIES_DESCRIPTOR); + defineProperty(OBJECTS_DIRECTORIES_DESCRIPTOR); + overridePropertiesFromEnv(); + } +} diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ASTExpression.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ASTExpression.java index a6573ce4a6..5f4fb07a96 100644 --- a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ASTExpression.java +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ASTExpression.java @@ -4,7 +4,18 @@ package net.sourceforge.pmd.lang.vf.ast; +import java.util.IdentityHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import net.sourceforge.pmd.lang.ast.Node; + public final class ASTExpression extends AbstractVfNode { + private static final Logger LOGGER = Logger.getLogger(ASTExpression.class.getName()); + ASTExpression(int id) { super(id); } @@ -13,4 +24,129 @@ public final class ASTExpression extends AbstractVfNode { protected R acceptVfVisitor(VfVisitor visitor, P data) { return visitor.visit(this, data); } + + private void logWarning(String warning, Node node) { + LOGGER.warning(warning + + ". nodeClass=" + node.getClass().getSimpleName() + // + ", fileName=" + AbstractTokenManager.getFileName() + + ", beginLine=" + node.getBeginLine() + + ", image=" + node.getImage()); + } + + /** + *

+ * An Expression can contain one or more strings that map to a piece of data. This method maps the string + * from the Visualforce page to terminal AST node that the string represents. The terminal node will be either an + * ASTIdentifier or ASTLiteral. It is the terminal node that is most important since it represents the type of data + * that will be displayed in the page. + *

+ *

+ * The string representation can be reconstructed by starting at the {@code Identifier} node and traversing its + * siblings until a node other than a {@code DotExpression} is encountered. Some more advanced situations aren't + * currently handled by this method. The method will throw an exception in such cases. + *

+ *
{@code
+     *  results in AST
+     * 
+     * The method would return key=ASTIdentifier(Image='MyValue'), value="MyValue"
+     * }
+ *
{@code
+     *  results in AST (It's important to notice that DotExpression is
+     * a sibling of Identifier.
+     * 
+     * 
+     *     
+     * 
+     * This method would return key=ASTIdentifier(Image='Text__c'), value="MyObject__c.Text__c"
+     * }
+ * + * THE FOLLOWING SITUATIONS ARE NOT HANDLED AND WILL THROW AN EXCEPTION. + * This syntax causes ambiguities with Apex Controller methods that return Maps versus accessing a CustomObject's + * field via array notation. This may be addressed in a future release. + * + *
{@code
+     *  results in AST
+     * 
+     * 
+     *     
+     * 
+
+     *  results in AST
+     * 
+     * 
+     *     
+     *         
+     *             
+     *         
+     *     
+     * 
+     * }
+ * + * @throws DataNodeStateException if the results of this method could have been incorrect. Callers should typically + * not rethrow this exception, as it will happen often and doesn't represent a terminal exception. + */ + public Map getDataNodes() throws DataNodeStateException { + Map result = new IdentityHashMap<>(); + + int numChildren = getNumChildren(); + List identifiers = findChildrenOfType(ASTIdentifier.class); + for (ASTIdentifier identifier : identifiers) { + LinkedList identifierNodes = new LinkedList<>(); + + // The Identifier is the first item that makes up the string + identifierNodes.add(identifier); + int index = identifier.getIndexInParent(); + + // Iterate through the rest of the children looking for ASTDotExpression nodes. + // The Image value of these nodes will be used to reconstruct the string. Any other node encountered will + // cause the while loop to break. The content of identifierNodes is used to construct the string and map + // it to the last element in identifierNodes. + index++; + while (index < numChildren) { + final Node node = getChild(index); + if (node instanceof ASTDotExpression) { + // The next part of the identifier will constructed from dot or array notation + if (node.getNumChildren() == 1) { + final Node expressionChild = node.getChild(0); + if (expressionChild instanceof ASTIdentifier || expressionChild instanceof ASTLiteral) { + identifierNodes.add((VfTypedNode) expressionChild); + } else { + // This should never happen + logWarning("Node expected to be Identifier or Literal", node); + throw new DataNodeStateException(); + } + } else { + // This should never happen + logWarning("More than one child found for ASTDotExpression", node); + throw new DataNodeStateException(); + } + } else if (node instanceof ASTExpression) { + // Not currently supported. This can occur in a couple of cases that may be supported in the future. + // 1. Custom Field using array notation. MyObject__c['Text__c'] + // 2. An Apex method that returns a map. ControllerMethod['KeyForMap'] + throw new DataNodeStateException(); + } else { + // Any other node type is not considered part of the identifier and breaks out of the loop + break; + } + index++; + } + + // Convert the list of nodes to a string representation, store the last node in the list as the map's key + String idString = String.join(".", identifierNodes.stream() + .map(i -> i.getImage()) + .collect(Collectors.toList())); + result.put(identifierNodes.getLast(), idString); + } + return result; + } + + + /** + * Thrown in cases where the the Identifiers in this node aren't ALL successfully parsed in a call to + * {@link #getDataNodes()} + */ + public static final class DataNodeStateException extends Exception { + } + } diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ASTIdentifier.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ASTIdentifier.java index da76df713f..496c68da43 100644 --- a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ASTIdentifier.java +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ASTIdentifier.java @@ -4,7 +4,7 @@ package net.sourceforge.pmd.lang.vf.ast; -public final class ASTIdentifier extends AbstractVfNode { +public final class ASTIdentifier extends AbstractVFDataNode { ASTIdentifier(int id) { super(id); diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ASTLiteral.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ASTLiteral.java index a472a282ed..c35099148c 100644 --- a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ASTLiteral.java +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ASTLiteral.java @@ -4,7 +4,7 @@ package net.sourceforge.pmd.lang.vf.ast; -public final class ASTLiteral extends AbstractVfNode { +public final class ASTLiteral extends AbstractVFDataNode { ASTLiteral(int id) { super(id); diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/AbstractVFDataNode.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/AbstractVFDataNode.java new file mode 100644 index 0000000000..94a7a4b951 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/AbstractVFDataNode.java @@ -0,0 +1,28 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import net.sourceforge.pmd.lang.vf.DataType; + +/** + * Represents a node that displays a piece of data. + */ +abstract class AbstractVFDataNode extends AbstractVfNode implements VfTypedNode { + + private DataType dataType; + + AbstractVFDataNode(int id) { + super(id); + } + + @Override + public DataType getDataType() { + return dataType; + } + + void setDataType(DataType dataType) { + this.dataType = dataType; + } +} diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypes.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypes.java new file mode 100644 index 0000000000..7b1eed7462 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypes.java @@ -0,0 +1,99 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.logging.Logger; + +import org.apache.commons.lang3.exception.ContextedRuntimeException; +import org.apache.commons.lang3.tuple.Pair; + +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.lang.Parser; +import net.sourceforge.pmd.lang.ParserOptions; +import net.sourceforge.pmd.lang.apex.ApexLanguageModule; +import net.sourceforge.pmd.lang.apex.ast.ApexNode; +import net.sourceforge.pmd.lang.ast.Node; +import net.sourceforge.pmd.lang.vf.DataType; + +import apex.jorje.semantic.symbol.type.BasicType; + +/** + * Responsible for storing a mapping of Apex Class properties that can be referenced from Visualforce to the type of the + * property. + */ +class ApexClassPropertyTypes extends SalesforceFieldTypes { + private static final Logger LOGGER = Logger.getLogger(ApexClassPropertyTypes.class.getName()); + private static final String APEX_CLASS_FILE_SUFFIX = ".cls"; + + /** + * Looks in {@code apexDirectories} for an Apex property identified by {@code expression}. + */ + @Override + public void findDataType(String expression, List apexDirectories) { + String[] parts = expression.split("\\."); + if (parts.length >= 2) { + // Load the class and parse it + String className = parts[0]; + + for (Path apexDirectory : apexDirectories) { + Path apexFilePath = apexDirectory.resolve(className + APEX_CLASS_FILE_SUFFIX); + if (Files.exists(apexFilePath) && Files.isRegularFile(apexFilePath)) { + Parser parser = getApexParser(); + try (BufferedReader reader = Files.newBufferedReader(apexFilePath, StandardCharsets.UTF_8)) { + Node node = parser.parse(apexFilePath.toString(), reader); + ApexClassPropertyTypesVisitor visitor = new ApexClassPropertyTypesVisitor(); + visitor.visit((ApexNode) node, null); + for (Pair variable : visitor.getVariables()) { + putDataType(variable.getKey(), DataType.fromBasicType(variable.getValue())); + } + } catch (IOException e) { + throw new ContextedRuntimeException(e) + .addContextValue("expression", expression) + .addContextValue("apexFilePath", apexFilePath); + } + + if (containsExpression(expression)) { + // Break out of the loop if a variable was found + break; + } + } + } + } + } + + @Override + protected DataType putDataType(String name, DataType dataType) { + DataType previousType = super.putDataType(name, dataType); + if (previousType != null && !previousType.equals(dataType)) { + // It is possible to have a property and method with different types that appear the same to this code. An + // example is an Apex class with a property "public String Foo {get; set;}" and a method of + // "Integer getFoo() { return 1; }". In this case set the value as Unknown because we can't be sure which it + // is. This code could be more complex in an attempt to determine if all the types are safe from escaping, + // but we will allow a false positive in order to let the user know that the code could be refactored to be + // more clear. + super.putDataType(name, DataType.Unknown); + LOGGER.warning("Conflicting types for " + + name + + ". CurrentType=" + + dataType + + ", PreviousType=" + + previousType); + } + return previousType; + } + + private Parser getApexParser() { + LanguageVersion languageVersion = LanguageRegistry.getLanguage(ApexLanguageModule.NAME).getDefaultVersion(); + ParserOptions parserOptions = languageVersion.getLanguageVersionHandler().getDefaultParserOptions(); + return languageVersion.getLanguageVersionHandler().getParser(parserOptions); + } +} diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypesVisitor.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypesVisitor.java new file mode 100644 index 0000000000..5190aac350 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypesVisitor.java @@ -0,0 +1,91 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.tuple.Pair; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTModifierNode; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClass; +import net.sourceforge.pmd.lang.apex.ast.ApexNode; +import net.sourceforge.pmd.lang.apex.ast.ApexParserVisitorAdapter; + +import apex.jorje.semantic.symbol.member.method.Generated; +import apex.jorje.semantic.symbol.member.method.MethodInfo; +import apex.jorje.semantic.symbol.type.BasicType; + +/** + * Visits an Apex class to determine a mapping of referenceable expressions to expression type. + */ +final class ApexClassPropertyTypesVisitor extends ApexParserVisitorAdapter { + + /** + * Prefix for standard bean type getters, i.e. getFoo + */ + private static final String BEAN_GETTER_PREFIX = "get"; + /** + * This is the prefix assigned to automatic get/set properties such as String myProp { get; set; } + */ + private static final String PROPERTY_PREFIX_ACCESSOR = "__sfdc_"; + + private static final String RETURN_TYPE_VOID = "void"; + + /** + * Pairs of (variableName, BasicType) + */ + private final List> variables; + + ApexClassPropertyTypesVisitor() { + this.variables = new ArrayList<>(); + } + + public List> getVariables() { + return this.variables; + } + + /** + * Stores the return type of the method in {@link #variables} if the method is referenceable from a + * Visualforce page. + */ + @Override + public Object visit(ASTMethod node, Object data) { + MethodInfo mi = node.getNode().getMethodInfo(); + if (mi.getParameterTypes().isEmpty() + && isVisibleToVisualForce(node) + && !RETURN_TYPE_VOID.equalsIgnoreCase(mi.getReturnType().getApexName()) + && (mi.getGenerated().equals(Generated.USER) || mi.isPropertyAccessor())) { + StringBuilder sb = new StringBuilder(); + List parents = node.getParentsOfType(ASTUserClass.class); + Collections.reverse(parents); + for (ASTUserClass parent : parents) { + sb.append(parent.getImage()).append("."); + } + String name = node.getImage(); + for (String prefix : new String[]{BEAN_GETTER_PREFIX, PROPERTY_PREFIX_ACCESSOR}) { + if (name.startsWith(prefix)) { + name = name.substring(prefix.length()); + } + } + sb.append(name); + + variables.add(Pair.of(sb.toString(), mi.getReturnType().getBasicType())); + } + return super.visit((ApexNode) node, data); + } + + /** + * Used to filter out methods that aren't visible to the Visualforce page. + * + * @return true if the method is visible to Visualforce. + */ + private boolean isVisibleToVisualForce(ASTMethod node) { + ASTModifierNode modifier = node.getFirstChildOfType(ASTModifierNode.class); + return modifier.isGlobal() | modifier.isPublic(); + } +} diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes.java new file mode 100644 index 0000000000..ac156c2b6f --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes.java @@ -0,0 +1,255 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.apache.commons.lang3.exception.ContextedRuntimeException; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import net.sourceforge.pmd.lang.vf.DataType; + +/** + * Responsible for storing a mapping of Fields that can be referenced from Visualforce to the type of the field. + */ +class ObjectFieldTypes extends SalesforceFieldTypes { + private static final Logger LOGGER = Logger.getLogger(ObjectFieldTypes.class.getName()); + + public static final String CUSTOM_OBJECT_SUFFIX = "__c"; + private static final String FIELDS_DIRECTORY = "fields"; + private static final String MDAPI_OBJECT_FILE_SUFFIX = ".object"; + private static final String SFDX_FIELD_FILE_SUFFIX = ".field-meta.xml"; + + private static final Map STANDARD_FIELD_TYPES; + + static { + STANDARD_FIELD_TYPES = new HashMap<>(); + STANDARD_FIELD_TYPES.put("createdbyid", DataType.Lookup); + STANDARD_FIELD_TYPES.put("createddate", DataType.DateTime); + STANDARD_FIELD_TYPES.put("id", DataType.Lookup); + STANDARD_FIELD_TYPES.put("isdeleted", DataType.Checkbox); + STANDARD_FIELD_TYPES.put("lastmodifiedbyid", DataType.Lookup); + STANDARD_FIELD_TYPES.put("lastmodifieddate", DataType.DateTime); + STANDARD_FIELD_TYPES.put("name", DataType.Text); + STANDARD_FIELD_TYPES.put("systemmodstamp", DataType.DateTime); + } + + /** + * Keep track of which ".object" files have already been processed. All fields are processed at once. If an object + * file has been processed + */ + private final Set objectFileProcessed; + + // XML Parsing objects + private final DocumentBuilder documentBuilder; + private final XPathExpression customObjectFieldsExpression; + private final XPathExpression customFieldFullNameExpression; + private final XPathExpression customFieldTypeExpression; + private final XPathExpression sfdxCustomFieldFullNameExpression; + private final XPathExpression sfdxCustomFieldTypeExpression; + + ObjectFieldTypes() { + this.objectFileProcessed = new HashSet<>(); + + try { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(false); + documentBuilderFactory.setValidating(false); + documentBuilderFactory.setIgnoringComments(true); + documentBuilderFactory.setIgnoringElementContentWhitespace(true); + documentBuilderFactory.setExpandEntityReferences(false); + documentBuilderFactory.setCoalescing(false); + documentBuilderFactory.setXIncludeAware(false); + documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); + documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new RuntimeException(e); + } + + try { + XPath xPath = XPathFactory.newInstance().newXPath(); + this.customObjectFieldsExpression = xPath.compile("/CustomObject/fields"); + this.customFieldFullNameExpression = xPath.compile("fullName/text()"); + this.customFieldTypeExpression = xPath.compile("type/text()"); + this.sfdxCustomFieldFullNameExpression = xPath.compile("/CustomField/fullName/text()"); + this.sfdxCustomFieldTypeExpression = xPath.compile("/CustomField/type/text()"); + } catch (XPathExpressionException e) { + throw new RuntimeException(e); + } + } + + /** + * Looks in {@code objectsDirectories} for a custom field identified by {@code expression}. + */ + @Override + protected void findDataType(String expression, List objectsDirectories) { + // The expression should be in the form . + String[] parts = expression.split("\\."); + if (parts.length == 1) { + throw new RuntimeException("Malformed identifier: " + expression); + } else if (parts.length == 2) { + String objectName = parts[0]; + String fieldName = parts[1]; + + addStandardFields(objectName); + + // Attempt to find a metadata file that contains the custom field. The information will be located in a + // file located at /.object or in an file located at + // //fields/.field-meta.xml. The list of object directories + // defaults to the [/../objects] but can be overridden by the user. + for (Path objectsDirectory : objectsDirectories) { + Path sfdxCustomFieldPath = getSfdxCustomFieldPath(objectsDirectory, objectName, fieldName); + if (sfdxCustomFieldPath != null) { + // SFDX Format + parseSfdxCustomField(objectName, sfdxCustomFieldPath); + } else { + // MDAPI Format + String fileName = objectName + MDAPI_OBJECT_FILE_SUFFIX; + Path mdapiPath = objectsDirectory.resolve(fileName); + if (Files.exists(mdapiPath) && Files.isRegularFile(mdapiPath)) { + parseMdapiCustomObject(mdapiPath); + } + } + + if (containsExpression(expression)) { + // Break out of the loop if a variable was found + break; + } + } + } else { + // TODO: Support cross object relationships, these are expressions that contain "__r" + LOGGER.fine("Expression does not have two parts: " + expression); + } + } + + /** + * Sfdx projects decompose custom fields into individual files. This method will return the individual file that + * corresponds to <objectName>.<fieldName> if it exists. + * + * @return path to the metadata file for the Custom Field or null if not found + */ + private Path getSfdxCustomFieldPath(Path objectsDirectory, String objectName, String fieldName) { + Path fieldsDirectoryPath = Paths.get(objectsDirectory.toString(), objectName, FIELDS_DIRECTORY); + if (Files.exists(fieldsDirectoryPath) && Files.isDirectory(fieldsDirectoryPath)) { + Path sfdxFieldPath = Paths.get(fieldsDirectoryPath.toString(), fieldName + SFDX_FIELD_FILE_SUFFIX); + if (Files.exists(sfdxFieldPath) && Files.isRegularFile(sfdxFieldPath)) { + return sfdxFieldPath; + } + } + return null; + } + + /** + * Determine the type of the custom field. + */ + private void parseSfdxCustomField(String customObjectName, Path sfdxCustomFieldPath) { + try { + Document document = documentBuilder.parse(sfdxCustomFieldPath.toFile()); + Node fullNameNode = (Node) sfdxCustomFieldFullNameExpression.evaluate(document, XPathConstants.NODE); + Node typeNode = (Node) sfdxCustomFieldTypeExpression.evaluate(document, XPathConstants.NODE); + String type = typeNode.getNodeValue(); + DataType dataType = DataType.fromString(type); + + String key = customObjectName + "." + fullNameNode.getNodeValue(); + putDataType(key, dataType); + } catch (IOException | SAXException | XPathExpressionException e) { + throw new ContextedRuntimeException(e) + .addContextValue("customObjectName", customObjectName) + .addContextValue("sfdxCustomFieldPath", sfdxCustomFieldPath); + } + } + + /** + * Parse the custom object path and determine the type of all of its custom fields. + */ + private void parseMdapiCustomObject(Path mdapiObjectFile) { + String fileName = mdapiObjectFile.getFileName().toString(); + + String customObjectName = fileName.substring(0, fileName.lastIndexOf(MDAPI_OBJECT_FILE_SUFFIX)); + if (!objectFileProcessed.contains(customObjectName)) { + try { + Document document = documentBuilder.parse(mdapiObjectFile.toFile()); + NodeList fieldsNodes = (NodeList) customObjectFieldsExpression.evaluate(document, XPathConstants.NODESET); + for (int i = 0; i < fieldsNodes.getLength(); i++) { + Node fieldsNode = fieldsNodes.item(i); + Node fullNameNode = (Node) customFieldFullNameExpression.evaluate(fieldsNode, XPathConstants.NODE); + if (fullNameNode == null) { + throw new RuntimeException("fullName evaluate failed for " + customObjectName + " " + fieldsNode.getTextContent()); + } + String name = fullNameNode.getNodeValue(); + if (endsWithIgnoreCase(name, CUSTOM_OBJECT_SUFFIX)) { + Node typeNode = (Node) customFieldTypeExpression.evaluate(fieldsNode, XPathConstants.NODE); + if (typeNode == null) { + throw new RuntimeException("type evaluate failed for object=" + customObjectName + ", field=" + name + " " + fieldsNode.getTextContent()); + } + String type = typeNode.getNodeValue(); + DataType dataType = DataType.fromString(type); + String key = customObjectName + "." + fullNameNode.getNodeValue(); + putDataType(key, dataType); + } + } + } catch (IOException | SAXException | XPathExpressionException e) { + throw new ContextedRuntimeException(e) + .addContextValue("customObjectName", customObjectName) + .addContextValue("mdapiObjectFile", mdapiObjectFile); + } + objectFileProcessed.add(customObjectName); + } + } + + /** + * Add the set of standard fields which aren't present in the metadata file, but may be refernced from the + * visualforce page. + */ + private void addStandardFields(String customObjectName) { + for (Map.Entry entry : STANDARD_FIELD_TYPES.entrySet()) { + putDataType(customObjectName + "." + entry.getKey(), entry.getValue()); + } + } + + /** + * Null safe endsWithIgnoreCase + */ + private boolean endsWithIgnoreCase(String str, String suffix) { + return str != null && str.toLowerCase(Locale.ROOT).endsWith(suffix.toLowerCase(Locale.ROOT)); + } + + @Override + protected DataType putDataType(String name, DataType dataType) { + DataType previousType = super.putDataType(name, dataType); + if (previousType != null && !previousType.equals(dataType)) { + // It should not be possible to have conflicting types for CustomFields + throw new RuntimeException("Conflicting types for " + + name + + ". CurrentType=" + + dataType + + ", PreviousType=" + + previousType); + } + return previousType; + } +} diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/SalesforceFieldTypes.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/SalesforceFieldTypes.java new file mode 100644 index 0000000000..3cbf975533 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/SalesforceFieldTypes.java @@ -0,0 +1,101 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import net.sourceforge.pmd.lang.vf.DataType; + +/** + * Responsible for storing a mapping of Fields that can be referenced from Visualforce to the type of the field. The + * fields are identified by in a case insensitive manner. + */ +abstract class SalesforceFieldTypes { + /** + * Cache of lowercase variable names to the variable type declared in the field's metadata file. + */ + private final Map variableNameToVariableType; + + /** + * Keep track of which variables were already processed. Avoid processing if a page repeatedly asks for an entry + * which we haven't previously found. + */ + private final Set variableNameProcessed; + + SalesforceFieldTypes() { + this.variableNameToVariableType = new HashMap<>(); + this.variableNameProcessed = new HashSet<>(); + } + + /** + * + * @param expression expression literal as declared in the Visualforce page + * @param vfFileName file name of the Visualforce page that contains expression. Used to resolve relative paths + * included in {@code metadataDirectories} + * @param metadataDirectories absolute or relative list of directories that may contain the metadata corresponding + * to {@code expression} + * @return the DataType if it can be determined, else null + */ + public DataType getDataType(String expression, String vfFileName, List metadataDirectories) { + String lowerExpression = expression.toLowerCase(Locale.ROOT); + if (variableNameToVariableType.containsKey(lowerExpression)) { + // The expression has been previously retrieved + return variableNameToVariableType.get(lowerExpression); + } else if (variableNameProcessed.contains(lowerExpression)) { + // The expression has been previously requested, but was not found + return null; + } else { + Path vfFilePath = Paths.get(vfFileName); + List resolvedPaths = new ArrayList<>(); + for (String metadataDirectory : metadataDirectories) { + if (Paths.get(metadataDirectory).isAbsolute()) { + resolvedPaths.add(Paths.get(metadataDirectory)); + } else { + resolvedPaths.add(vfFilePath.getParent().resolve(metadataDirectory)); + } + } + + findDataType(expression, resolvedPaths); + variableNameProcessed.add(lowerExpression); + return variableNameToVariableType.get(lowerExpression); + } + } + + /** + * Stores {@link DataType} in a map using lower cased {@code expression} as the key. + * @param expression expression literal as declared in the Visualforce page + * @param dataType identifier determined for + * @return the previous value associated with {@code key}, or {@code null} if there was no mapping for {@code key}. + */ + protected DataType putDataType(String expression, DataType dataType) { + return variableNameToVariableType.put(expression.toLowerCase(Locale.ROOT), dataType); + } + + /** + * @return true if the expression has previously been stored via {@link #putDataType(String, DataType)} + */ + protected boolean containsExpression(String expression) { + return variableNameToVariableType.containsKey(expression.toLowerCase(Locale.ROOT)); + } + + /** + * Subclasses should attempt to find the {@code DataType} of {@code expression} within + * {@code metadataDirectories}. The subclass should store the value by invoking + * {@link #putDataType(String, DataType)}. + * + * @param expression expression as defined in the Visualforce page, case is preserved + * @param metadataDirectories list of directories that may contain the metadata corresponding to {@code expression} + */ + protected abstract void findDataType(String expression, List metadataDirectories); +} + diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfAstInternals.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfAstInternals.java new file mode 100644 index 0000000000..e45aae08a2 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfAstInternals.java @@ -0,0 +1,23 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import net.sourceforge.pmd.annotation.InternalApi; +import net.sourceforge.pmd.lang.vf.DataType; + +/** + * This is internal API, and can be changed at any time. + */ +@InternalApi +public final class VfAstInternals { + + private VfAstInternals() { + // utility class + } + + public static void setDataType(VfTypedNode node, DataType dataType) { + ((AbstractVFDataNode) node).setDataType(dataType); + } +} diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor.java new file mode 100644 index 0000000000..532c0f0eb4 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor.java @@ -0,0 +1,174 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; + +import net.sourceforge.pmd.lang.vf.DataType; +import net.sourceforge.pmd.lang.vf.VfParserOptions; + +/** + * Visits {@link ASTExpression} nodes and stores type information for + * {@link net.sourceforge.pmd.lang.vf.ast.ASTIdentifier} children that represent an IdentifierDotted construct. An + * IdentifierDotted is of the form {@code MyObject__c.MyField__c}. + */ +class VfExpressionTypeVisitor extends VfParserVisitorAdapter { + private static final Logger LOGGER = Logger.getLogger(VfExpressionTypeVisitor.class.getName()); + + private static final String APEX_PAGE = "apex:page"; + private static final String CONTROLLER_ATTRIBUTE = "controller"; + private static final String STANDARD_CONTROLLER_ATTRIBUTE = "standardcontroller"; + private static final String EXTENSIONS_ATTRIBUTE = "extensions"; + + private final ApexClassPropertyTypes apexClassPropertyTypes; + private final ObjectFieldTypes objectFieldTypes; + private final String fileName; + + private String standardControllerName; + + /** + * List of all Apex Class names that the VF page might refer to. These values come from either the + * {@code controller} or {@code extensions} attribute. + */ + private final List apexClassNames; + private final List apexDirectories; + private final List objectsDirectories; + + VfExpressionTypeVisitor(String fileName, VfParserOptions propertySource) { + this.fileName = fileName; + this.apexDirectories = propertySource.getProperty(VfParserOptions.APEX_DIRECTORIES_DESCRIPTOR); + this.objectsDirectories = propertySource.getProperty(VfParserOptions.OBJECTS_DIRECTORIES_DESCRIPTOR); + this.apexClassNames = new ArrayList<>(); + this.apexClassPropertyTypes = new ApexClassPropertyTypes(); + this.objectFieldTypes = new ObjectFieldTypes(); + } + + @Override + public Object visit(ASTCompilationUnit node, Object data) { + if (StringUtils.isBlank(fileName)) { + // Skip visiting if there isn't a file that can anchor the directories + return data; + } + + if (apexDirectories.isEmpty() && objectsDirectories.isEmpty()) { + // Skip visiting if there aren't any directories to look in + return data; + } + return super.visit(node, data); + } + + /** + * Gather names of Controller, Extensions, and StandardController. Each of these may contain the identifier + * referenced from the Visualforce page. + */ + @Override + public Object visit(ASTElement node, Object data) { + if (APEX_PAGE.equalsIgnoreCase(node.getName())) { + List attribs = node.findChildrenOfType(ASTAttribute.class); + + for (ASTAttribute attr : attribs) { + String lowerAttr = attr.getName().toLowerCase(Locale.ROOT); + if (CONTROLLER_ATTRIBUTE.equals(lowerAttr)) { + // Controller Name should always take precedence + apexClassNames.add(0, attr.getFirstChildOfType(ASTAttributeValue.class) + .getFirstChildOfType(ASTText.class).getImage()); + break; + } else if (STANDARD_CONTROLLER_ATTRIBUTE.equals(lowerAttr)) { + standardControllerName = attr.getFirstChildOfType(ASTAttributeValue.class) + .getFirstChildOfType(ASTText.class).getImage().toLowerCase(Locale.ROOT); + } else if (EXTENSIONS_ATTRIBUTE.equalsIgnoreCase(lowerAttr)) { + for (String extension : attr.getFirstChildOfType(ASTAttributeValue.class) + .getFirstChildOfType(ASTText.class).getImage().split(",")) { + apexClassNames.add(extension.trim()); + } + } + } + } + return super.visit(node, data); + } + + /** + * Invoke {@link ASTExpression#getDataNodes()} on all children of {@code node} and attempt to determine the + * {@link DataType} by looking at Apex or CustomField metadata. + */ + @Override + public Object visit(ASTElExpression node, Object data) { + for (Map.Entry entry : getDataNodeNames(node).entrySet()) { + String name = entry.getValue(); + DataType type = null; + String[] parts = name.split("\\."); + + // Apex extensions take precedence over Standard controllers. + // The example below will display "Name From Inner Class" instead of the Account name + // public class AccountExtension { + // public AccountExtension(ApexPages.StandardController controller) { + // } + // + // public InnerClass getAccount() { + // return new InnerClass(); + // } + // + // public class InnerClass { + // public String getName() { + // return 'Name From Inner Class'; + // } + // } + // } + // + // + // + + // Try to find the identifier in an Apex class + for (String apexClassName : apexClassNames) { + String fullName = apexClassName + "." + name; + type = apexClassPropertyTypes.getDataType(fullName, fileName, apexDirectories); + if (type != null) { + break; + } + } + + // Try to find the identifier in a CustomField if it wasn't found in an Apex class and the identifier corresponds + // to the StandardController. + if (type == null) { + if (parts.length >= 2 && standardControllerName != null && standardControllerName.equalsIgnoreCase(parts[0])) { + type = objectFieldTypes.getDataType(name, fileName, objectsDirectories); + } + } + + if (type != null) { + VfAstInternals.setDataType(entry.getKey(), type); + } else { + LOGGER.fine("Unable to determine type for: " + name); + } + } + return super.visit(node, data); + } + + /** + * Invoke {@link ASTExpression#getDataNodes()} for all {@link ASTExpression} children of {@code node} and return + * the consolidated results. + */ + private IdentityHashMap getDataNodeNames(ASTElExpression node) { + IdentityHashMap dataNodeToName = new IdentityHashMap<>(); + + for (ASTExpression expression : node.findChildrenOfType(ASTExpression.class)) { + try { + dataNodeToName.putAll(expression.getDataNodes()); + } catch (ASTExpression.DataNodeStateException ignore) { + // Intentionally left blank + continue; + } + } + + return dataNodeToName; + } +} diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfParser.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfParser.java index b0a94e1b0b..571b7bf37c 100644 --- a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfParser.java +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfParser.java @@ -10,6 +10,7 @@ import net.sourceforge.pmd.lang.ast.CharStream; import net.sourceforge.pmd.lang.ast.ParseException; import net.sourceforge.pmd.lang.ast.impl.javacc.JavaccTokenDocument; import net.sourceforge.pmd.lang.ast.impl.javacc.JjtreeParserAdapter; +import net.sourceforge.pmd.lang.vf.VfParserOptions; /** * Parser for the VisualForce language. @@ -28,7 +29,13 @@ public final class VfParser extends JjtreeParserAdapter { @Override protected ASTCompilationUnit parseImpl(CharStream cs, ParserTask task) throws ParseException { - return new VfParserImpl(cs).CompilationUnit().makeTaskInfo(task); + ASTCompilationUnit root = new VfParserImpl(cs).CompilationUnit().makeTaskInfo(task); + + // Add type information to the AST + VfExpressionTypeVisitor visitor = new VfExpressionTypeVisitor(task); + visitor.visit(root, null); + + return root; } } diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfTypedNode.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfTypedNode.java new file mode 100644 index 0000000000..8d0d74b6d1 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/VfTypedNode.java @@ -0,0 +1,22 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import net.sourceforge.pmd.lang.vf.DataType; + +/** + * Represents a node that displays a piece of data. + */ +public interface VfTypedNode extends VfNode { + + /** + * Returns the data type this node refers to. A null value indicates that no matching Metadata was found for this + * node. null differs from {@link DataType#Unknown} which indicates that Metadata was found but it wasn't mappable + * to one of the enums. + * + *

Example XPath 1.0 and 2.0: {@code //Identifier[@DataType='DateTime']} + */ + DataType getDataType(); +} diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElRule.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElRule.java index fe71477123..1a26fa49e3 100644 --- a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElRule.java +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElRule.java @@ -12,6 +12,7 @@ import java.util.Set; import java.util.regex.Pattern; import net.sourceforge.pmd.lang.ast.Node; +import net.sourceforge.pmd.lang.vf.DataType; import net.sourceforge.pmd.lang.vf.ast.ASTArguments; import net.sourceforge.pmd.lang.vf.ast.ASTAttribute; import net.sourceforge.pmd.lang.vf.ast.ASTContent; @@ -25,6 +26,7 @@ import net.sourceforge.pmd.lang.vf.ast.ASTLiteral; import net.sourceforge.pmd.lang.vf.ast.ASTNegationExpression; import net.sourceforge.pmd.lang.vf.ast.ASTText; import net.sourceforge.pmd.lang.vf.ast.VfNode; +import net.sourceforge.pmd.lang.vf.ast.VfTypedNode; import net.sourceforge.pmd.lang.vf.rule.AbstractVfRule; /** @@ -53,7 +55,6 @@ public class VfUnescapeElRule extends AbstractVfRule { @Override public Object visit(ASTHtmlScript node, Object data) { checkIfCorrectlyEscaped(node, data); - return super.visit(node, data); } @@ -409,8 +410,11 @@ public class VfUnescapeElRule extends AbstractVfRule { continue; } - final List ids = expr.findChildrenOfType(ASTIdentifier.class); + if (expressionContainsSafeDataNodes(expr)) { + continue; + } + final List ids = expr.findChildrenOfType(ASTIdentifier.class); for (final ASTIdentifier id : ids) { boolean isEscaped = false; @@ -442,6 +446,24 @@ public class VfUnescapeElRule extends AbstractVfRule { return !nonEscapedIds.isEmpty(); } + /** + * Return true if the type of all data nodes can be determined and none of them require escaping + */ + private boolean expressionContainsSafeDataNodes(ASTExpression expression) { + try { + for (VfTypedNode node : expression.getDataNodes().keySet()) { + DataType dataType = node.getDataType(); + if (dataType == null || dataType.requiresEscaping) { + return false; + } + } + + return true; + } catch (ASTExpression.DataNodeStateException e) { + return false; + } + } + private boolean containsSafeFields(final VfNode expression) { final ASTExpression ex = expression.getFirstChildOfType(ASTExpression.class); diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/DataTypeTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/DataTypeTest.java new file mode 100644 index 0000000000..0accbd9625 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/DataTypeTest.java @@ -0,0 +1,38 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import apex.jorje.semantic.symbol.type.BasicType; + +public class DataTypeTest { + @Test + public void testFromString() { + assertEquals(DataType.AutoNumber, DataType.fromString("AutoNumber")); + assertEquals(DataType.AutoNumber, DataType.fromString("autonumber")); + assertEquals(DataType.Unknown, DataType.fromString("")); + assertEquals(DataType.Unknown, DataType.fromString(null)); + } + + @Test + public void testFromBasicType() { + assertEquals(DataType.Checkbox, DataType.fromBasicType(BasicType.BOOLEAN)); + assertEquals(DataType.Number, DataType.fromBasicType(BasicType.DECIMAL)); + assertEquals(DataType.Number, DataType.fromBasicType(BasicType.DOUBLE)); + assertEquals(DataType.Unknown, DataType.fromBasicType(BasicType.APEX_OBJECT)); + assertEquals(DataType.Unknown, DataType.fromBasicType(null)); + } + + @Test + public void testRequiresEncoding() { + assertFalse(DataType.AutoNumber.requiresEscaping); + assertTrue(DataType.Text.requiresEscaping); + } +} diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/RuleSetFactoryTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/RuleSetFactoryTest.java index f478885c12..3ffadc6a4a 100644 --- a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/RuleSetFactoryTest.java +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/RuleSetFactoryTest.java @@ -5,7 +5,10 @@ package net.sourceforge.pmd.lang.vf; import net.sourceforge.pmd.AbstractRuleSetFactoryTest; +import net.sourceforge.pmd.lang.apex.ApexLanguageModule; public class RuleSetFactoryTest extends AbstractRuleSetFactoryTest { - // no additional tests + public RuleSetFactoryTest() { + super(ApexLanguageModule.TERSE_NAME); + } } diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/VFTestUtils.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/VFTestUtils.java new file mode 100644 index 0000000000..ea6980dcb2 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/VFTestUtils.java @@ -0,0 +1,66 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf; + +import java.nio.file.Path; +import java.nio.file.Paths; + +public final class VFTestUtils { + private VFTestUtils() { + } + + /** + * Salesforce metadata is stored in two different formats, the newer sfdx form and the older mdapi format. Used to + * locate metadata on the file system during unit tests. + */ + public enum MetadataFormat { + SFDX("sfdx"), + MDAPI("mdapi"); + + public final String directoryName; + + MetadataFormat(String directoryName) { + this.directoryName = directoryName; + } + } + + /** + * Represents the metadata types that are referenced from unit tests. Used to locate metadata on the file system + * during unit tests. + */ + public enum MetadataType { + Apex("classes"), + Objects("objects"), + Vf("pages"); + + public final String directoryName; + + MetadataType(String directoryName) { + this.directoryName = directoryName; + } + } + + /** + * @return the path of the directory that matches the given parameters. The directory path is constructed using the + * following convention: + * src/test/resources/_decomposed_test_package_name_/_test_class_name_minus_Test_/metadata/_metadata_format_/_metadata_type_ + */ + public static Path getMetadataPath(Object testClazz, MetadataFormat metadataFormat, MetadataType metadataType) { + Path path = Paths.get("src", "test", "resources"); + // Decompose the test's package structure into directories + for (String directory : testClazz.getClass().getPackage().getName().split("\\.")) { + path = path.resolve(directory); + } + // Remove 'Test' from the class name + path = path.resolve(testClazz.getClass().getSimpleName().replaceFirst("Test$", "")); + // Append additional directories based on the MetadataFormat and MetadataType + path = path.resolve("metadata").resolve(metadataFormat.directoryName); + if (metadataType != null) { + path = path.resolve(metadataType.directoryName); + } + + return path.toAbsolutePath(); + } +} diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/VfParserOptionsTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/VfParserOptionsTest.java new file mode 100644 index 0000000000..5c238b1157 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/VfParserOptionsTest.java @@ -0,0 +1,22 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class VfParserOptionsTest { + + @Test + public void testDefaultPropertyDescriptors() { + VfParserOptions vfParserOptions = new VfParserOptions(); + assertEquals(VfParserOptions.DEFAULT_APEX_DIRECTORIES, + vfParserOptions.getProperty(VfParserOptions.APEX_DIRECTORIES_DESCRIPTOR)); + assertEquals(VfParserOptions.DEFAULT_OBJECT_DIRECTORIES, + vfParserOptions.getProperty(VfParserOptions.OBJECTS_DIRECTORIES_DESCRIPTOR)); + } + +} diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ASTExpressionTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ASTExpressionTest.java new file mode 100644 index 0000000000..803b1e83c5 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ASTExpressionTest.java @@ -0,0 +1,240 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.Test; + +import net.sourceforge.pmd.lang.ast.Node; +import net.sourceforge.pmd.util.treeexport.XmlTreeRenderer; + +public class ASTExpressionTest { + + /** + * Slightly different scenarios which cause different AST, but should return the same results. + */ + private static final String[] SNIPPET_TEMPLATES = new String[] { + "{!%s}", + "", + ""}; + + @Test + public void testExpressionWithApexGetter() throws ASTExpression.DataNodeStateException { + for (String template : SNIPPET_TEMPLATES) { + ASTCompilationUnit compilationUnit = compile(String.format(template, "MyValue")); + + List nodes = getExpressions(compilationUnit); + assertEquals(template, 1, nodes.size()); + + ASTExpression expression = (ASTExpression) nodes.get(0); + Map identifiers = expression.getDataNodes(); + assertEquals(template, 1, identifiers.size()); + + Map map = invertMap(identifiers); + assertTrue(template, map.containsKey("MyValue")); + assertTrue(template, map.get("MyValue") instanceof ASTIdentifier); + } + } + + @Test + public void testExpressionWithStandardController() throws ASTExpression.DataNodeStateException { + for (String template : SNIPPET_TEMPLATES) { + ASTCompilationUnit compilationUnit = compile(String.format(template, "MyObject__c.Text__c")); + + List nodes = getExpressions(compilationUnit); + assertEquals(template, 1, nodes.size()); + + ASTExpression expression = (ASTExpression) nodes.get(0); + Map identifiers = expression.getDataNodes(); + assertEquals(template, 1, identifiers.size()); + + Map map = invertMap(identifiers); + assertTrue(template, map.containsKey("MyObject__c.Text__c")); + assertTrue(template, map.get("MyObject__c.Text__c") instanceof ASTIdentifier); + } + } + + @Test + public void testSelectOptions() throws ASTExpression.DataNodeStateException { + for (String template : SNIPPET_TEMPLATES) { + ASTCompilationUnit compilationUnit = compile(String.format(template, "userOptions.0")); + + List nodes = getExpressions(compilationUnit); + assertEquals(template, 1, nodes.size()); + + ASTExpression expression = (ASTExpression) nodes.get(0); + Map identifiers = expression.getDataNodes(); + assertEquals(template, 1, identifiers.size()); + + Map map = invertMap(identifiers); + assertTrue(template, map.containsKey("userOptions.0")); + assertTrue(template, map.get("userOptions.0") instanceof ASTLiteral); + } + } + + @Test + public void testMultipleIdentifiers() throws ASTExpression.DataNodeStateException { + for (String template : SNIPPET_TEMPLATES) { + ASTCompilationUnit compilationUnit = compile(String.format(template, "MyObject__c.Text__c + ' this is a string' + MyObject__c.Text2__c")); + + List nodes = getExpressions(compilationUnit); + assertEquals(template, 1, nodes.size()); + + ASTExpression expression = (ASTExpression) nodes.get(0); + Map identifiers = expression.getDataNodes(); + assertEquals(template, 2, identifiers.size()); + + Map map = invertMap(identifiers); + assertEquals(template, 2, map.size()); + assertTrue(template, map.containsKey("MyObject__c.Text__c")); + assertTrue(template, map.get("MyObject__c.Text__c") instanceof ASTIdentifier); + assertTrue(template, map.containsKey("MyObject__c.Text2__c")); + assertTrue(template, map.get("MyObject__c.Text2__c") instanceof ASTIdentifier); + } + } + + @Test + public void testIdentifierWithRelation() throws ASTExpression.DataNodeStateException { + for (String template : SNIPPET_TEMPLATES) { + ASTCompilationUnit compilationUnit = compile(String.format(template, "MyObject1__c.MyObject2__r.Text__c")); + + List nodes = getExpressions(compilationUnit); + assertEquals(template, 1, nodes.size()); + + ASTExpression expression = (ASTExpression) nodes.get(0); + Map identifiers = expression.getDataNodes(); + assertEquals(template, 1, identifiers.size()); + + Map map = invertMap(identifiers); + assertEquals(template, 1, map.size()); + assertTrue(template, map.containsKey("MyObject1__c.MyObject2__r.Text__c")); + assertTrue(template, map.get("MyObject1__c.MyObject2__r.Text__c") instanceof ASTIdentifier); + } + } + + @Test + public void testMultipleIdentifiersWithRelation() throws ASTExpression.DataNodeStateException { + for (String template : SNIPPET_TEMPLATES) { + ASTCompilationUnit compilationUnit = compile(String.format(template, "MyObject1__c.MyObject2__r.Text__c + ' this is a string' + MyObject1__c.MyObject2__r.Text2__c")); + + List nodes = getExpressions(compilationUnit); + assertEquals(template, 1, nodes.size()); + + ASTExpression expression = (ASTExpression) nodes.get(0); + Map identifiers = expression.getDataNodes(); + assertEquals(template, 2, identifiers.size()); + + Map map = invertMap(identifiers); + assertEquals(template, 2, map.size()); + assertTrue(template, map.containsKey("MyObject1__c.MyObject2__r.Text__c")); + assertTrue(template, map.get("MyObject1__c.MyObject2__r.Text__c") instanceof ASTIdentifier); + assertTrue(template, map.containsKey("MyObject1__c.MyObject2__r.Text2__c")); + assertTrue(template, map.get("MyObject1__c.MyObject2__r.Text2__c") instanceof ASTIdentifier); + } + } + + /** + * The current implementation does not support expressing statements using array notation. This notation introduces + * complexities that may be addressed in a future release. + */ + @Test + public void testExpressionWithArrayIndexingNotSupported() { + for (String template : SNIPPET_TEMPLATES) { + ASTCompilationUnit compilationUnit = compile(String.format(template, "MyObject__c['Name']")); + + List nodes = getExpressions(compilationUnit); + assertEquals(template, 2, nodes.size()); + + ASTExpression expression = (ASTExpression) nodes.get(0); + try { + expression.getDataNodes(); + fail(template + " should have thrown"); + } catch (ASTExpression.DataNodeStateException expected) { + // Intentionally left blank + } + } + } + + @Test + public void testIdentifierWithRelationIndexedAsArrayNotSupported() { + for (String template : SNIPPET_TEMPLATES) { + ASTCompilationUnit compilationUnit = compile(String.format(template, "MyObject1__c['MyObject2__r'].Text__c")); + + List nodes = getExpressions(compilationUnit); + assertEquals(template, 2, nodes.size()); + + ASTExpression expression = (ASTExpression) nodes.get(0); + try { + expression.getDataNodes(); + fail(template + " should have thrown"); + } catch (ASTExpression.DataNodeStateException expected) { + // Intentionally left blank + } + } + } + + @Test + public void testIdentifierWithComplexIndexedArrayNotSupported() { + for (String template : SNIPPET_TEMPLATES) { + ASTCompilationUnit compilationUnit = compile(String.format(template, "theLineItems[item.Id].UnitPrice")); + + List nodes = getExpressions(compilationUnit); + assertEquals(template, 2, nodes.size()); + + ASTExpression expression = (ASTExpression) nodes.get(0); + try { + expression.getDataNodes(); + fail(template + " should have thrown"); + } catch (ASTExpression.DataNodeStateException expected) { + // Intentionally left blank + } + } + } + + private static List getExpressions(ASTCompilationUnit compilationUnit) { + return compilationUnit.descendants(ASTExpression.class).toList(it -> it); + } + + /** + * Invert the map to make it easier to unit test. + */ + private Map invertMap(Map map) { + Map result = map.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); + // Ensure no values have been lost + assertEquals(map.size(), result.size()); + return result; + } + + private ASTCompilationUnit compile(String snippet) { + return compile(snippet, false); + } + + private ASTCompilationUnit compile(String snippet, boolean renderAST) { + ASTCompilationUnit node = VfParsingHelper.DEFAULT.parse( + "" + + snippet + + "" + ); + + if (renderAST) { + try { + new XmlTreeRenderer().renderSubtree(node, System.out); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + return node; + } +} diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/AbstractVfNodesTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/AbstractVfTest.java similarity index 84% rename from pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/AbstractVfNodesTest.java rename to pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/AbstractVfTest.java index d905a3817a..ab922a2abf 100644 --- a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/AbstractVfNodesTest.java +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/AbstractVfTest.java @@ -4,7 +4,7 @@ package net.sourceforge.pmd.lang.vf.ast; -public abstract class AbstractVfNodesTest { +public abstract class AbstractVfTest { protected final VfParsingHelper vf = VfParsingHelper.DEFAULT.withResourceContext(getClass()); diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypesTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypesTest.java new file mode 100644 index 0000000000..d8e14a70b8 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypesTest.java @@ -0,0 +1,74 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import static org.junit.Assert.assertNull; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import net.sourceforge.pmd.lang.vf.DataType; +import net.sourceforge.pmd.lang.vf.VFTestUtils; +import net.sourceforge.pmd.lang.vf.VfParserOptions; + +public class ApexClassPropertyTypesTest { + private static final Map EXPECTED_DATA_TYPES; + + static { + // Intentionally use the wrong case for property names to ensure that they can be found. The Apex class name + // must have the correct case since it is used to lookup the file. The Apex class name is guaranteed to be correct + // in the Visualforce page, but the property names are not + EXPECTED_DATA_TYPES = new HashMap<>(); + EXPECTED_DATA_TYPES.put("ApexController.accOuntIdProp", DataType.Lookup); + EXPECTED_DATA_TYPES.put("ApexController.AcCountId", DataType.Lookup); + EXPECTED_DATA_TYPES.put("ApexController.AcCountname", DataType.Text); + + // InnerController + // The class should be parsed to Unknown. It's not a valid expression on its own. + EXPECTED_DATA_TYPES.put("ApexController.innErController", DataType.Unknown); + EXPECTED_DATA_TYPES.put("ApexController.innErController.innErAccountIdProp", DataType.Lookup); + EXPECTED_DATA_TYPES.put("ApexController.innErController.innErAccountid", DataType.Lookup); + EXPECTED_DATA_TYPES.put("ApexController.innErController.innErAccountnAme", DataType.Text); + + // Edge cases + // Invalid class should return null + EXPECTED_DATA_TYPES.put("unknownclass.invalidProperty", null); + // Invalid class property should return null + EXPECTED_DATA_TYPES.put("ApexController.invalidProperty", null); + /* + * It is possible to have a property and method with different types that resolve to the same Visualforce + * expression. An example is an Apex class with a property "public String Foo {get; set;}" and a method of + * "Integer getFoo() { return 1; }". These properties should map to {@link DataType#Unknown}. + */ + EXPECTED_DATA_TYPES.put("ApexController.ConflictingProp", DataType.Unknown); + } + + @Test + public void testApexClassIsProperlyParsed() { + Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf) + .resolve("SomePage.page"); + ApexClassPropertyTypes apexClassPropertyTypes = new ApexClassPropertyTypes(); + + ObjectFieldTypesTest.validateDataTypes(EXPECTED_DATA_TYPES, apexClassPropertyTypes, vfPagePath, + VfParserOptions.APEX_DIRECTORIES_DESCRIPTOR.defaultValue()); + } + + @Test + public void testInvalidDirectoryDoesNotCauseAnException() { + Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf) + .resolve("SomePage.page"); + String vfFileName = vfPagePath.toString(); + + List paths = Arrays.asList(Paths.get("..", "classes-does-not-exist").toString()); + ApexClassPropertyTypes apexClassPropertyTypes = new ApexClassPropertyTypes(); + assertNull(apexClassPropertyTypes.getDataType("ApexController.accOuntIdProp", vfFileName, paths)); + } +} diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypesVisitorTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypesVisitorTest.java new file mode 100644 index 0000000000..4ec20b3dc8 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypesVisitorTest.java @@ -0,0 +1,67 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Test; + +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.lang.Parser; +import net.sourceforge.pmd.lang.ParserOptions; +import net.sourceforge.pmd.lang.apex.ApexLanguageModule; +import net.sourceforge.pmd.lang.apex.ast.ApexNode; +import net.sourceforge.pmd.lang.ast.Node; +import net.sourceforge.pmd.lang.vf.VFTestUtils; + +import apex.jorje.semantic.symbol.type.BasicType; + +public class ApexClassPropertyTypesVisitorTest { + @Test + public void testApexClassIsProperlyParsed() throws IOException { + LanguageVersion languageVersion = LanguageRegistry.getLanguage(ApexLanguageModule.NAME).getDefaultVersion(); + ParserOptions parserOptions = languageVersion.getLanguageVersionHandler().getDefaultParserOptions(); + Parser parser = languageVersion.getLanguageVersionHandler().getParser(parserOptions); + + Path apexPath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Apex) + .resolve("ApexController.cls").toAbsolutePath(); + ApexClassPropertyTypesVisitor visitor = new ApexClassPropertyTypesVisitor(); + try (BufferedReader reader = Files.newBufferedReader(apexPath, StandardCharsets.UTF_8)) { + Node node = parser.parse(apexPath.toString(), reader); + assertNotNull(node); + visitor.visit((ApexNode) node, null); + } + + List> variables = visitor.getVariables(); + assertEquals(7, variables.size()); + Map variableNameToVariableType = new Hashtable<>(); + for (Pair variable : variables) { + // Map the values and ensure there were no duplicates + BasicType previous = variableNameToVariableType.put(variable.getKey(), variable.getValue()); + assertNull(variable.getKey(), previous); + } + + assertEquals(BasicType.ID, variableNameToVariableType.get("ApexController.AccountIdProp")); + assertEquals(BasicType.ID, variableNameToVariableType.get("ApexController.AccountId")); + assertEquals(BasicType.STRING, variableNameToVariableType.get("ApexController.AccountName")); + assertEquals(BasicType.APEX_OBJECT, variableNameToVariableType.get("ApexController.InnerController")); + assertEquals(BasicType.ID, variableNameToVariableType.get("ApexController.InnerController.InnerAccountIdProp")); + assertEquals(BasicType.ID, variableNameToVariableType.get("ApexController.InnerController.InnerAccountId")); + assertEquals(BasicType.STRING, variableNameToVariableType.get("ApexController.InnerController.InnerAccountName")); + } +} diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypesTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypesTest.java new file mode 100644 index 0000000000..899b2f7012 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypesTest.java @@ -0,0 +1,131 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import net.sourceforge.pmd.lang.vf.DataType; +import net.sourceforge.pmd.lang.vf.VFTestUtils; +import net.sourceforge.pmd.lang.vf.VfParserOptions; + +public class ObjectFieldTypesTest { + private static final Map EXPECTED_SFDX_DATA_TYPES; + private static final Map EXPECTED_MDAPI_DATA_TYPES; + + static { + EXPECTED_SFDX_DATA_TYPES = new HashMap<>(); + EXPECTED_SFDX_DATA_TYPES.put("Account.Checkbox__c", DataType.Checkbox); + EXPECTED_SFDX_DATA_TYPES.put("Account.DateTime__c", DataType.DateTime); + EXPECTED_SFDX_DATA_TYPES.put("Account.LongTextArea__c", DataType.LongTextArea); + EXPECTED_SFDX_DATA_TYPES.put("Account.Picklist__c", DataType.Picklist); + EXPECTED_SFDX_DATA_TYPES.put("Account.Text__c", DataType.Text); + EXPECTED_SFDX_DATA_TYPES.put("Account.TextArea__c", DataType.TextArea); + // Edge Cases + // Invalid property should return null + EXPECTED_SFDX_DATA_TYPES.put("Account.DoesNotExist__c", null); + + EXPECTED_MDAPI_DATA_TYPES = new HashMap<>(); + EXPECTED_MDAPI_DATA_TYPES.put("Account.MDCheckbox__c", DataType.Checkbox); + EXPECTED_MDAPI_DATA_TYPES.put("Account.MDDateTime__c", DataType.DateTime); + EXPECTED_MDAPI_DATA_TYPES.put("Account.MDLongTextArea__c", DataType.LongTextArea); + EXPECTED_MDAPI_DATA_TYPES.put("Account.MDPicklist__c", DataType.Picklist); + EXPECTED_MDAPI_DATA_TYPES.put("Account.MDText__c", DataType.Text); + EXPECTED_MDAPI_DATA_TYPES.put("Account.MDTextArea__c", DataType.TextArea); + // Edge Cases + // Invalid property should return null + EXPECTED_MDAPI_DATA_TYPES.put("Account.DoesNotExist__c", null); + } + + /** + * Verify that CustomFields stored in sfdx project format are correctly parsed + */ + @Test + public void testSfdxAccountIsProperlyParsed() { + Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf).resolve("SomePage.page"); + + ObjectFieldTypes objectFieldTypes = new ObjectFieldTypes(); + validateSfdxAccount(objectFieldTypes, vfPagePath, VfParserOptions.OBJECTS_DIRECTORIES_DESCRIPTOR.defaultValue()); + } + + /** + * Verify that CustomFields stored in mdapi format are correctly parsed + */ + @Test + public void testMdapiAccountIsProperlyParsed() { + Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.MDAPI, VFTestUtils.MetadataType.Vf).resolve("SomePage.page"); + + ObjectFieldTypes objectFieldTypes = new ObjectFieldTypes(); + validateMDAPIAccount(objectFieldTypes, vfPagePath, VfParserOptions.OBJECTS_DIRECTORIES_DESCRIPTOR.defaultValue()); + } + + /** + * Verify that fields are found across multiple directories + */ + @Test + public void testFieldsAreFoundInMultipleDirectories() { + ObjectFieldTypes objectFieldTypes; + Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf) + .resolve("SomePage.page"); + + List paths = Arrays.asList(VfParserOptions.OBJECTS_DIRECTORIES_DESCRIPTOR.defaultValue().get(0), + VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.MDAPI, VFTestUtils.MetadataType.Objects).toString()); + objectFieldTypes = new ObjectFieldTypes(); + validateSfdxAccount(objectFieldTypes, vfPagePath, paths); + validateMDAPIAccount(objectFieldTypes, vfPagePath, paths); + + Collections.reverse(paths); + objectFieldTypes = new ObjectFieldTypes(); + validateSfdxAccount(objectFieldTypes, vfPagePath, paths); + validateMDAPIAccount(objectFieldTypes, vfPagePath, paths); + } + + @Test + public void testInvalidDirectoryDoesNotCauseAnException() { + Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf).resolve("SomePage.page"); + String vfFileName = vfPagePath.toString(); + + List paths = Arrays.asList(Paths.get("..", "objects-does-not-exist").toString()); + ObjectFieldTypes objectFieldTypes = new ObjectFieldTypes(); + assertNull(objectFieldTypes.getDataType("Account.DoesNotExist__c", vfFileName, paths)); + } + + /** + * Validate the expected results when the Account Fields are stored in decomposed sfdx format + */ + private void validateSfdxAccount(ObjectFieldTypes objectFieldTypes, Path vfPagePath, List paths) { + validateDataTypes(EXPECTED_SFDX_DATA_TYPES, objectFieldTypes, vfPagePath, paths); + } + + /** + * Validate the expected results when the Account Fields are stored in a single file MDAPI format + */ + private void validateMDAPIAccount(ObjectFieldTypes objectFieldTypes, Path vfPagePath, List paths) { + validateDataTypes(EXPECTED_MDAPI_DATA_TYPES, objectFieldTypes, vfPagePath, paths); + } + + /** + * Verify that return values of {@link SalesforceFieldTypes#getDataType(String, String, List)} using the keys of + * {@code expectedDataTypes} matches the values of {@code expectedDataTypes} + */ + public static void validateDataTypes(Map expectedDataTypes, SalesforceFieldTypes fieldTypes, + Path vfPagePath, List paths) { + String vfFileName = vfPagePath.toString(); + + for (Map.Entry entry : expectedDataTypes.entrySet()) { + assertEquals(entry.getKey(), entry.getValue(), fieldTypes.getDataType(entry.getKey(), vfFileName, paths)); + } + } +} diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfDocStyleTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfDocStyleTest.java index 4fe6959f8f..7b0677d1d7 100644 --- a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfDocStyleTest.java +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfDocStyleTest.java @@ -24,7 +24,7 @@ import org.junit.Test; * @author sergey.gorbaty - VF adaptation * */ -public class VfDocStyleTest extends AbstractVfNodesTest { +public class VfDocStyleTest extends AbstractVfTest { /** * Smoke test for VF parser. diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitorTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitorTest.java new file mode 100644 index 0000000000..9a24cfeaf9 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitorTest.java @@ -0,0 +1,196 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.ast; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.junit.Test; + +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.lang.Parser; +import net.sourceforge.pmd.lang.ParserOptions; +import net.sourceforge.pmd.lang.ast.Node; +import net.sourceforge.pmd.lang.ast.NodeStream; +import net.sourceforge.pmd.lang.vf.DataType; +import net.sourceforge.pmd.lang.vf.VFTestUtils; +import net.sourceforge.pmd.lang.vf.VfLanguageModule; +import net.sourceforge.pmd.util.treeexport.XmlTreeRenderer; + +public class VfExpressionTypeVisitorTest { + private static final Map EXPECTED_CUSTOM_FIELD_DATA_TYPES; + private static final Map EXPECTED_APEX_DATA_TYPES; + + static { + EXPECTED_CUSTOM_FIELD_DATA_TYPES = new HashMap<>(); + EXPECTED_CUSTOM_FIELD_DATA_TYPES.put("CreatedDate", DataType.DateTime); + EXPECTED_CUSTOM_FIELD_DATA_TYPES.put("DateTime__c", DataType.DateTime); + EXPECTED_CUSTOM_FIELD_DATA_TYPES.put("Checkbox__c", DataType.Checkbox); + EXPECTED_CUSTOM_FIELD_DATA_TYPES.put("Name", DataType.Text); + EXPECTED_CUSTOM_FIELD_DATA_TYPES.put("Text__c", DataType.Text); + EXPECTED_CUSTOM_FIELD_DATA_TYPES.put("TextArea__c", DataType.TextArea); + EXPECTED_CUSTOM_FIELD_DATA_TYPES.put("LongTextArea__c", DataType.LongTextArea); + EXPECTED_CUSTOM_FIELD_DATA_TYPES.put("Picklist__c", DataType.Picklist); + + EXPECTED_APEX_DATA_TYPES = new HashMap<>(); + EXPECTED_APEX_DATA_TYPES.put("AccountIdProp", DataType.Lookup); + EXPECTED_APEX_DATA_TYPES.put("AccountId", DataType.Lookup); + EXPECTED_APEX_DATA_TYPES.put("InnerAccountId", DataType.Lookup); + EXPECTED_APEX_DATA_TYPES.put("InnerAccountIdProp", DataType.Lookup); + EXPECTED_APEX_DATA_TYPES.put("AccountName", DataType.Text); + EXPECTED_APEX_DATA_TYPES.put("InnerAccountName", DataType.Text); + EXPECTED_APEX_DATA_TYPES.put("ConflictingProp", DataType.Unknown); + } + + /** + * Strings that use dot notation(Account.CreatedDate) result in ASTIdentifier nodes + */ + @Test + public void testXpathQueryForCustomFieldIdentifiers() throws FileNotFoundException { + Node rootNode = compile("StandardAccount.page"); + + for (Map.Entry entry : EXPECTED_CUSTOM_FIELD_DATA_TYPES.entrySet()) { + List nodes = getIdentifiers(rootNode, entry); + + // Each string appears twice, it is set on a "value" attribute and inline + assertEquals(entry.getKey(), 2, nodes.size()); + for (Node node : nodes) { + assertEquals(entry.getKey(), node.getImage()); + assertTrue(node.getClass().getSimpleName(), node instanceof ASTIdentifier); + ASTIdentifier identifier = (ASTIdentifier) node; + assertEquals(entry.getKey(), entry.getValue(), identifier.getDataType()); + } + } + } + + /** + * Strings that use array notation, Account['CreatedDate') don't have a DataType added. This type of notation + * creates ambiguous situations with Apex methods that return Maps. This may be addressed in a future release. + */ + @Test + public void testXpathQueryForCustomFieldLiteralsHaveNullDataType() throws FileNotFoundException { + Node rootNode = compile("StandardAccount.page"); + + for (Map.Entry entry : EXPECTED_CUSTOM_FIELD_DATA_TYPES.entrySet()) { + List nodes = rootNode.descendants(ASTLiteral.class) + // Literals are surrounded by apostrophes + .filterMatching(ASTLiteral::getImage, "'" + entry.getKey() + "'") + .filterMatching(ASTLiteral::getDataType, null) + .toList(); + + // Each string appears twice, it is set on a "value" attribute and inline + assertEquals(entry.getKey(), 2, nodes.size()); + for (Node node : nodes) { + assertEquals(String.format("'%s'", entry.getKey()), node.getImage()); + assertTrue(node.getClass().getSimpleName(), node instanceof ASTLiteral); + ASTLiteral literal = (ASTLiteral) node; + assertEquals(entry.getKey(), null, literal.getDataType()); + } + } + } + + /** + * Nodes where the DataType can't be determined should have a null DataType + */ + @Test + public void testDataTypeForCustomFieldsNotFound() throws FileNotFoundException { + Node rootNode = compile("StandardAccount.page"); + + checkNodes(rootNode.descendants(ASTIdentifier.class) + .filterMatching(ASTIdentifier::getImage, "NotFoundField__c")); + checkNodes(rootNode.descendants(ASTLiteral.class) + .filterMatching(ASTLiteral::getImage, "'NotFoundField__c'")); + } + + private void checkNodes(NodeStream nodeStream) { + // Each string appears twice, it is set on a "value" attribute and inline + List nodes = nodeStream.toList(); + assertEquals(2, nodes.size()); + for (VfTypedNode node : nodes) { + assertNull(node.getDataType()); + } + } + + /** + * Apex properties result in ASTIdentifier nodes + */ + @Test + public void testXpathQueryForProperties() throws FileNotFoundException { + Node rootNode = compile("ApexController.page"); + + for (Map.Entry entry : EXPECTED_APEX_DATA_TYPES.entrySet()) { + List nodes = getIdentifiers(rootNode, entry); + + // Each string appears twice, it is set on a "value" attribute and inline + assertEquals(entry.getKey(), 2, nodes.size()); + for (Node node : nodes) { + assertEquals(entry.getKey(), node.getImage()); + assertTrue(node.getClass().getSimpleName(), node instanceof ASTIdentifier); + ASTIdentifier identifier = (ASTIdentifier) node; + assertEquals(entry.getKey(), entry.getValue(), identifier.getDataType()); + } + } + } + + private List getIdentifiers(Node rootNode, Entry entry) { + return rootNode.descendants(ASTIdentifier.class) + .filterMatching(ASTIdentifier::getImage, entry.getKey()) + .filterMatching(ASTIdentifier::getDataType, entry.getValue()) + .toList(); + } + + /** + * Nodes where the DataType can't be determined should have a null DataType + */ + @Test + public void testDataTypeForApexPropertiesNotFound() throws FileNotFoundException { + Node rootNode = compile("ApexController.page"); + + // Each string appears twice, it is set on a "value" attribute and inline + checkNodes(rootNode.descendants(ASTIdentifier.class) + .filterMatching(ASTIdentifier::getImage, "NotFoundProp")); + } + + private Node compile(String pageName) throws FileNotFoundException { + return compile(pageName, false); + } + + private Node compile(String pageName, boolean renderAST) throws FileNotFoundException { + Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf) + .resolve(pageName); + return compile(vfPagePath, renderAST); + } + + private Node compile(Path vfPagePath, boolean renderAST) throws FileNotFoundException { + LanguageVersion languageVersion = LanguageRegistry.getLanguage(VfLanguageModule.NAME).getDefaultVersion(); + ParserOptions parserOptions = languageVersion.getLanguageVersionHandler().getDefaultParserOptions(); + Parser parser = languageVersion.getLanguageVersionHandler().getParser(parserOptions); + + Node node = parser.parse(vfPagePath.toString(), new FileReader(vfPagePath.toFile())); + assertNotNull(node); + + if (renderAST) { + try { + new XmlTreeRenderer().renderSubtree(node, System.out); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + return node; + } +} diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfPageStyleTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfPageStyleTest.java index 77287f6e42..e5578adc46 100644 --- a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfPageStyleTest.java +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfPageStyleTest.java @@ -10,7 +10,7 @@ import java.util.List; import org.junit.Test; -public class VfPageStyleTest extends AbstractVfNodesTest { +public class VfPageStyleTest extends AbstractVfTest { /** * Test parsing of a EL expression. diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfParserTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfParserTest.java index 6ce22de09c..76a609c289 100644 --- a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfParserTest.java +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/VfParserTest.java @@ -11,7 +11,7 @@ import net.sourceforge.pmd.lang.ast.ParseException; /** * @author sergey.gorbaty */ -public class VfParserTest extends AbstractVfNodesTest { +public class VfParserTest extends AbstractVfTest { @Test public void testSingleDoubleQuoteAndEL() { diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java index 40212b810e..e58545d614 100644 --- a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java @@ -4,8 +4,160 @@ package net.sourceforge.pmd.lang.vf.rule.security; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import org.junit.Test; + +import net.sourceforge.pmd.PMD; +import net.sourceforge.pmd.PMDException; +import net.sourceforge.pmd.Report; +import net.sourceforge.pmd.Rule; +import net.sourceforge.pmd.RuleContext; +import net.sourceforge.pmd.RuleSet; +import net.sourceforge.pmd.RuleSets; +import net.sourceforge.pmd.RuleViolation; +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.lang.Parser; +import net.sourceforge.pmd.lang.ParserOptions; +import net.sourceforge.pmd.lang.ast.Node; +import net.sourceforge.pmd.lang.vf.VFTestUtils; +import net.sourceforge.pmd.lang.vf.VfLanguageModule; import net.sourceforge.pmd.testframework.PmdRuleTst; public class VfUnescapeElTest extends PmdRuleTst { - // no additional unit tests + public static final String EXPECTED_RULE_MESSAGE = "Avoid unescaped user controlled content in EL"; + + /** + * Verify that CustomFields stored in sfdx project format are correctly parsed + */ + @Test + public void testSfdxCustomFields() throws IOException, PMDException { + Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf) + .resolve("StandardAccount.page"); + + Report report = runRule(vfPagePath); + List ruleViolations = report.getViolations(); + assertEquals("Number of violations", 20, ruleViolations.size()); + + int firstLineWithErrors = 14; + for (int i = 0; i < ruleViolations.size(); i++) { + RuleViolation ruleViolation = ruleViolations.get(i); + assertEquals(EXPECTED_RULE_MESSAGE, ruleViolation.getDescription()); + int expectedLineNumber = firstLineWithErrors + i; + if ((ruleViolations.size() + firstLineWithErrors - 1) == expectedLineNumber) { + // The last line has two errors on the same page + expectedLineNumber = expectedLineNumber - 1; + } + assertEquals("Line Number", expectedLineNumber, ruleViolation.getBeginLine()); + } + } + + /** + * Verify that CustomFields stored in mdapi format are correctly parsed + */ + @Test + public void testMdapiCustomFields() throws IOException, PMDException { + Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.MDAPI, VFTestUtils.MetadataType.Vf).resolve("StandardAccount.page"); + + Report report = runRule(vfPagePath); + List ruleViolations = report.getViolations(); + assertEquals("Number of violations", 6, ruleViolations.size()); + int firstLineWithErrors = 8; + for (int i = 0; i < ruleViolations.size(); i++) { + RuleViolation ruleViolation = ruleViolations.get(i); + assertEquals(EXPECTED_RULE_MESSAGE, ruleViolation.getDescription()); + assertEquals("Line Number", firstLineWithErrors + i, ruleViolation.getBeginLine()); + } + } + + /** + * Tests a page with a single Apex controller + */ + @Test + public void testApexController() throws IOException, PMDException { + Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf).resolve("ApexController.page"); + + Report report = runRule(vfPagePath); + List ruleViolations = report.getViolations(); + assertEquals("Number of violations", 2, ruleViolations.size()); + int firstLineWithErrors = 9; + for (int i = 0; i < ruleViolations.size(); i++) { + // There should start at line 9 + RuleViolation ruleViolation = ruleViolations.get(i); + assertEquals(EXPECTED_RULE_MESSAGE, ruleViolation.getDescription()); + assertEquals("Line Number", firstLineWithErrors + i, ruleViolation.getBeginLine()); + } + } + + /** + * Tests a page with a standard controller and two Apex extensions + */ + @Test + public void testExtensions() throws IOException, PMDException { + Path vfPagePath = VFTestUtils.getMetadataPath(this, VFTestUtils.MetadataFormat.SFDX, VFTestUtils.MetadataType.Vf) + .resolve(Paths.get("StandardAccountWithExtensions.page")); + + Report report = runRule(vfPagePath); + List ruleViolations = report.getViolations(); + assertEquals(8, ruleViolations.size()); + int firstLineWithErrors = 9; + for (int i = 0; i < ruleViolations.size(); i++) { + RuleViolation ruleViolation = ruleViolations.get(i); + assertEquals(EXPECTED_RULE_MESSAGE, ruleViolation.getDescription()); + assertEquals(firstLineWithErrors + i, ruleViolation.getBeginLine()); + } + } + + /** + * Runs a rule against a Visualforce page on the file system. This code is based on + * {@link net.sourceforge.pmd.testframework.RuleTst#runTestFromString(String, Rule, Report, LanguageVersion, boolean)} + */ + private Report runRule(Path vfPagePath) throws FileNotFoundException, PMDException { + LanguageVersion languageVersion = LanguageRegistry.getLanguage(VfLanguageModule.NAME).getDefaultVersion(); + ParserOptions parserOptions = languageVersion.getLanguageVersionHandler().getDefaultParserOptions(); + Parser parser = languageVersion.getLanguageVersionHandler().getParser(parserOptions); + + Node node = parser.parse(vfPagePath.toString(), new FileReader(vfPagePath.toFile())); + assertNotNull(node); + + // BEGIN Based on RuleTst class + PMD p = new PMD(); + p.getConfiguration().setDefaultLanguageVersion(languageVersion); + p.getConfiguration().setIgnoreIncrementalAnalysis(true); + // simple class loader, that doesn't delegate to parent. + // this allows us in the tests to simulate PMD run without + // auxclasspath, not even the classes from the test dependencies + // will be found. + p.getConfiguration().setClassLoader(new ClassLoader() { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("java.") || name.startsWith("javax.")) { + return super.loadClass(name, resolve); + } + throw new ClassNotFoundException(name); + } + }); + + Rule rule = findRule("category/vf/security.xml", "VfUnescapeEl"); + Report report = new Report(); + RuleContext ctx = new RuleContext(); + ctx.setReport(report); + ctx.setSourceCodeFile(vfPagePath.toFile()); + ctx.setLanguageVersion(languageVersion); + ctx.setIgnoreExceptions(false); + RuleSet rules = RuleSet.forSingleRule(rule); + p.getSourceCodeProcessor().processSourceCode(new FileReader(vfPagePath.toFile()), new RuleSets(rules), ctx); + // END Based on RuleTst class + + return report; + } } diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypes/metadata/sfdx/classes/ApexController.cls b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypes/metadata/sfdx/classes/ApexController.cls new file mode 100644 index 0000000000..76fdac2c62 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypes/metadata/sfdx/classes/ApexController.cls @@ -0,0 +1,44 @@ +public class ApexController { + public Id AccountIdProp { get; set; } + public String ConflictingProp { get; set; } + + public ApexController() { + acc = [SELECT Id, Name, Site FROM Account + WHERE Id = :ApexPages.currentPage().getParameters().get('id')]; + this.AccountIdProp = acc.Id; + } + + public Id getAccountId() { + return acc.id; + } + + public String getAccountName() { + return acc.name; + } + + public Integer getConflictingProp() { + return ''; + } + + public InnerController getInnerController() { + return new InnerController(this); + } + + public class InnerController { + private ApexController parent; + public Id InnerAccountIdProp { get; set; } + + public InnerController(ApexController parent) { + this.parent = parent; + this.InnerAccountIdProp = parent.AccountIdProp; + } + + public Id getInnerAccountId() { + return 'Inner: ' + parent.acc.id; + } + + public String getInnerAccountName() { + return 'Inner: ' + parent.acc.name; + } + } +} \ No newline at end of file diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypes/metadata/sfdx/pages/SomePage.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypes/metadata/sfdx/pages/SomePage.page new file mode 100644 index 0000000000..c0090110b0 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypes/metadata/sfdx/pages/SomePage.page @@ -0,0 +1,3 @@ + +The contents of this page aren't relevant to the test, but the test requires the file to be present. + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypesVisitor/metadata/sfdx/classes/ApexController.cls b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypesVisitor/metadata/sfdx/classes/ApexController.cls new file mode 100644 index 0000000000..df449e3e6f --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ApexClassPropertyTypesVisitor/metadata/sfdx/classes/ApexController.cls @@ -0,0 +1,39 @@ +public class ApexController { + public Id AccountIdProp { get; set; } + + public ApexController() { + acc = [SELECT Id, Name, Site FROM Account + WHERE Id = :ApexPages.currentPage().getParameters().get('id')]; + this.AccountIdProp = acc.Id; + } + + public Id getAccountId() { + return acc.id; + } + + public String getAccountName() { + return acc.name; + } + + public InnerController getInnerController() { + return new InnerController(this); + } + + public class InnerController { + private ApexController parent; + public Id InnerAccountIdProp { get; set; } + + public InnerController(ApexController parent) { + this.parent = parent; + this.InnerAccountIdProp = parent.AccountIdProp; + } + + public Id getInnerAccountId() { + return 'Inner: ' + parent.acc.id; + } + + public String getInnerAccountName() { + return 'Inner: ' + parent.acc.name; + } + } +} \ No newline at end of file diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/mdapi/objects/Account.object b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/mdapi/objects/Account.object new file mode 100644 index 0000000000..5c37ef3c93 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/mdapi/objects/Account.object @@ -0,0 +1,445 @@ + + + + CallHighlightAction + Default + + + CallHighlightAction + Large + Default + + + CallHighlightAction + Small + Default + + + CancelEdit + Default + + + CancelEdit + Large + Default + + + CancelEdit + Small + Default + + + Delete + Default + + + Delete + Large + Default + + + Delete + Small + Default + + + Edit + Default + + + Edit + Large + Default + + + Edit + Small + Default + + + EmailHighlightAction + Default + + + EmailHighlightAction + Large + Default + + + EmailHighlightAction + Small + Default + + + EnableCustomerPortalUser + Default + + + EnableCustomerPortalUser + Large + Default + + + EnableCustomerPortalUser + Small + Default + + + List + Default + + + List + Large + Default + + + List + Small + Default + + + ListClean + Default + + + ListClean + Large + Default + + + ListClean + Small + Default + + + New + Default + + + New + Large + Default + + + New + Small + Default + + + RequestUpdate + Default + + + RequestUpdate + Large + Default + + + RequestUpdate + Small + Default + + + SaveEdit + Default + + + SaveEdit + Large + Default + + + SaveEdit + Small + Default + + + SmsHighlightAction + Default + + + SmsHighlightAction + Large + Default + + + SmsHighlightAction + Small + Default + + + Tab + Default + + + Tab + Large + Default + + + Tab + Small + Default + + + View + Default + + + View + Large + Default + + + View + Small + Default + + + ViewCustomerPortalUser + Default + + + ViewCustomerPortalUser + Large + Default + + + ViewCustomerPortalUser + Small + Default + + + WebsiteHighlightAction + Default + + + WebsiteHighlightAction + Large + Default + + + WebsiteHighlightAction + Small + Default + + SYSTEM + true + false + Private + + ACCOUNT.NAME + ACCOUNT.ADDRESS1_CITY + ACCOUNT.PHONE1 + ACCOUNT.NAME + ACCOUNT.SITE + CORE.USERS.ALIAS + ACCOUNT.TYPE + ACCOUNT.NAME + ACCOUNT.SITE + CORE.USERS.ALIAS + ACCOUNT.TYPE + ACCOUNT.PHONE1 + ACCOUNT.NAME + ACCOUNT.SITE + ACCOUNT.PHONE1 + CORE.USERS.ALIAS + + ReadWrite + + AccountNumber + false + + + AccountSource + false + Picklist + + + AnnualRevenue + false + + + BillingAddress + false + + + MDCheckbox__c + false + false + + false + Checkbox + + + CleanStatus + false + + + DandbCompanyId + false + Lookup + + + MDDateTime__c + false + + false + false + DateTime + + + Description + false + + + DunsNumber + false + + + Fax + false + + + Industry + false + Picklist + + + Jigsaw + false + + + NaicsCode + false + + + NaicsDesc + false + + + Name + true + + + NumberOfEmployees + false + + + OperatingHoursId + false + Lookup + + + OwnerId + true + Lookup + + + Ownership + false + Picklist + + + ParentId + false + Hierarchy + + + Phone + false + + + MDPicklist__c + false + + false + false + Picklist + + + false + + Value1 + false + + + + Value2 + false + + + + + + + Rating + false + Picklist + + + ShippingAddress + false + + + Sic + false + + + SicDesc + false + + + Site + false + + + MDLongTextArea__c + false + + 32768 + false + LongTextArea + 3 + + + MDTextArea__c + false + + false + false + TextArea + + + MDText__c + false + + 255 + false + false + Text + false + + + TickerSymbol + false + + + Tradestyle + false + + + Type + false + Picklist + + + Website + false + + + YearStarted + false + + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/mdapi/pages/SomePage.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/mdapi/pages/SomePage.page new file mode 100644 index 0000000000..c0090110b0 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/mdapi/pages/SomePage.page @@ -0,0 +1,3 @@ + +The contents of this page aren't relevant to the test, but the test requires the file to be present. + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/Checkbox__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/Checkbox__c.field-meta.xml new file mode 100644 index 0000000000..03ffc46229 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/Checkbox__c.field-meta.xml @@ -0,0 +1,9 @@ + + + Checkbox__c + false + false + + false + Checkbox + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/DateTime__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/DateTime__c.field-meta.xml new file mode 100644 index 0000000000..5e0ce1cd2c --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/DateTime__c.field-meta.xml @@ -0,0 +1,9 @@ + + + DateTime__c + false + + false + false + DateTime + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/LongTextArea__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/LongTextArea__c.field-meta.xml new file mode 100644 index 0000000000..6de4650b1f --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/LongTextArea__c.field-meta.xml @@ -0,0 +1,10 @@ + + + LongTextArea__c + false + + 32768 + false + LongTextArea + 3 + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/Picklist__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/Picklist__c.field-meta.xml new file mode 100644 index 0000000000..597d9d3d30 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/Picklist__c.field-meta.xml @@ -0,0 +1,24 @@ + + + Picklist__c + false + + false + false + Picklist + + + false + + Value1 + false + + + + Value2 + false + + + + + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/TextArea__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/TextArea__c.field-meta.xml new file mode 100644 index 0000000000..cd59083b9e --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/TextArea__c.field-meta.xml @@ -0,0 +1,9 @@ + + + TextArea__c + false + + false + false + TextArea + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/Text__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/Text__c.field-meta.xml new file mode 100644 index 0000000000..efeeb5262a --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/Text__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Text__c + false + + 255 + false + false + Text + false + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/pages/SomePage.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/pages/SomePage.page new file mode 100644 index 0000000000..c0090110b0 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/ObjectFieldTypes/metadata/sfdx/pages/SomePage.page @@ -0,0 +1,3 @@ + +The contents of this page aren't relevant to the test, but the test requires the file to be present. + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/classes/ApexController.cls b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/classes/ApexController.cls new file mode 100644 index 0000000000..76fdac2c62 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/classes/ApexController.cls @@ -0,0 +1,44 @@ +public class ApexController { + public Id AccountIdProp { get; set; } + public String ConflictingProp { get; set; } + + public ApexController() { + acc = [SELECT Id, Name, Site FROM Account + WHERE Id = :ApexPages.currentPage().getParameters().get('id')]; + this.AccountIdProp = acc.Id; + } + + public Id getAccountId() { + return acc.id; + } + + public String getAccountName() { + return acc.name; + } + + public Integer getConflictingProp() { + return ''; + } + + public InnerController getInnerController() { + return new InnerController(this); + } + + public class InnerController { + private ApexController parent; + public Id InnerAccountIdProp { get; set; } + + public InnerController(ApexController parent) { + this.parent = parent; + this.InnerAccountIdProp = parent.AccountIdProp; + } + + public Id getInnerAccountId() { + return 'Inner: ' + parent.acc.id; + } + + public String getInnerAccountName() { + return 'Inner: ' + parent.acc.name; + } + } +} \ No newline at end of file diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/Checkbox__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/Checkbox__c.field-meta.xml new file mode 100644 index 0000000000..03ffc46229 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/Checkbox__c.field-meta.xml @@ -0,0 +1,9 @@ + + + Checkbox__c + false + false + + false + Checkbox + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/DateTime__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/DateTime__c.field-meta.xml new file mode 100644 index 0000000000..5e0ce1cd2c --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/DateTime__c.field-meta.xml @@ -0,0 +1,9 @@ + + + DateTime__c + false + + false + false + DateTime + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/LongTextArea__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/LongTextArea__c.field-meta.xml new file mode 100644 index 0000000000..6de4650b1f --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/LongTextArea__c.field-meta.xml @@ -0,0 +1,10 @@ + + + LongTextArea__c + false + + 32768 + false + LongTextArea + 3 + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/Picklist__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/Picklist__c.field-meta.xml new file mode 100644 index 0000000000..597d9d3d30 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/Picklist__c.field-meta.xml @@ -0,0 +1,24 @@ + + + Picklist__c + false + + false + false + Picklist + + + false + + Value1 + false + + + + Value2 + false + + + + + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/TextArea__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/TextArea__c.field-meta.xml new file mode 100644 index 0000000000..cd59083b9e --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/TextArea__c.field-meta.xml @@ -0,0 +1,9 @@ + + + TextArea__c + false + + false + false + TextArea + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/Text__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/Text__c.field-meta.xml new file mode 100644 index 0000000000..efeeb5262a --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/Text__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Text__c + false + + 255 + false + false + Text + false + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/pages/ApexController.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/pages/ApexController.page new file mode 100644 index 0000000000..b876d5f46b --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/pages/ApexController.page @@ -0,0 +1,18 @@ + + + {!AccountIdProp} + + {!AccountId} + + {!InnerController.InnerAccountId} + + {!InnerController.InnerAccountIdProp} + + {!AccountName} + + {!InnerController.InnerAccountName} + + {!ConflictingProp} + + {!NotFoundProp} + \ No newline at end of file diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/pages/StandardAccount.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/pages/StandardAccount.page new file mode 100644 index 0000000000..67851687c1 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/ast/VfExpressionTypeVisitor/metadata/sfdx/pages/StandardAccount.page @@ -0,0 +1,41 @@ + + + + {!Account.CreatedDate} + + {!Account.Checkbox__c} + + {!Account.DateTime__c} + + {!Account.Name} + + {!Account.Text__c} + + {!Account.TextArea__c} + + {!Account.LongTextArea__c} + + {!Account.Picklist__c} + + {!Account.NotFoundField__c} + + + + {!Account['CreatedDate']} + + {!Account['Checkbox__c']} + + {!Account['DateTime__c']} + + {!Account['Name']} + + {!Account['Text__c']} + + {!Account['TextArea__c']} + + {!Account['LongTextArea__c']} + + {!Account['Picklist__c']} + + {!Account['NotFoundField__c']} + \ No newline at end of file diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/mdapi/objects/Account.object b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/mdapi/objects/Account.object new file mode 100644 index 0000000000..5c37ef3c93 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/mdapi/objects/Account.object @@ -0,0 +1,445 @@ + + + + CallHighlightAction + Default + + + CallHighlightAction + Large + Default + + + CallHighlightAction + Small + Default + + + CancelEdit + Default + + + CancelEdit + Large + Default + + + CancelEdit + Small + Default + + + Delete + Default + + + Delete + Large + Default + + + Delete + Small + Default + + + Edit + Default + + + Edit + Large + Default + + + Edit + Small + Default + + + EmailHighlightAction + Default + + + EmailHighlightAction + Large + Default + + + EmailHighlightAction + Small + Default + + + EnableCustomerPortalUser + Default + + + EnableCustomerPortalUser + Large + Default + + + EnableCustomerPortalUser + Small + Default + + + List + Default + + + List + Large + Default + + + List + Small + Default + + + ListClean + Default + + + ListClean + Large + Default + + + ListClean + Small + Default + + + New + Default + + + New + Large + Default + + + New + Small + Default + + + RequestUpdate + Default + + + RequestUpdate + Large + Default + + + RequestUpdate + Small + Default + + + SaveEdit + Default + + + SaveEdit + Large + Default + + + SaveEdit + Small + Default + + + SmsHighlightAction + Default + + + SmsHighlightAction + Large + Default + + + SmsHighlightAction + Small + Default + + + Tab + Default + + + Tab + Large + Default + + + Tab + Small + Default + + + View + Default + + + View + Large + Default + + + View + Small + Default + + + ViewCustomerPortalUser + Default + + + ViewCustomerPortalUser + Large + Default + + + ViewCustomerPortalUser + Small + Default + + + WebsiteHighlightAction + Default + + + WebsiteHighlightAction + Large + Default + + + WebsiteHighlightAction + Small + Default + + SYSTEM + true + false + Private + + ACCOUNT.NAME + ACCOUNT.ADDRESS1_CITY + ACCOUNT.PHONE1 + ACCOUNT.NAME + ACCOUNT.SITE + CORE.USERS.ALIAS + ACCOUNT.TYPE + ACCOUNT.NAME + ACCOUNT.SITE + CORE.USERS.ALIAS + ACCOUNT.TYPE + ACCOUNT.PHONE1 + ACCOUNT.NAME + ACCOUNT.SITE + ACCOUNT.PHONE1 + CORE.USERS.ALIAS + + ReadWrite + + AccountNumber + false + + + AccountSource + false + Picklist + + + AnnualRevenue + false + + + BillingAddress + false + + + MDCheckbox__c + false + false + + false + Checkbox + + + CleanStatus + false + + + DandbCompanyId + false + Lookup + + + MDDateTime__c + false + + false + false + DateTime + + + Description + false + + + DunsNumber + false + + + Fax + false + + + Industry + false + Picklist + + + Jigsaw + false + + + NaicsCode + false + + + NaicsDesc + false + + + Name + true + + + NumberOfEmployees + false + + + OperatingHoursId + false + Lookup + + + OwnerId + true + Lookup + + + Ownership + false + Picklist + + + ParentId + false + Hierarchy + + + Phone + false + + + MDPicklist__c + false + + false + false + Picklist + + + false + + Value1 + false + + + + Value2 + false + + + + + + + Rating + false + Picklist + + + ShippingAddress + false + + + Sic + false + + + SicDesc + false + + + Site + false + + + MDLongTextArea__c + false + + 32768 + false + LongTextArea + 3 + + + MDTextArea__c + false + + false + false + TextArea + + + MDText__c + false + + 255 + false + false + Text + false + + + TickerSymbol + false + + + Tradestyle + false + + + Type + false + Picklist + + + Website + false + + + YearStarted + false + + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/mdapi/pages/StandardAccount.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/mdapi/pages/StandardAccount.page new file mode 100644 index 0000000000..8413160a63 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/mdapi/pages/StandardAccount.page @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/classes/ApexController.cls b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/classes/ApexController.cls new file mode 100644 index 0000000000..df449e3e6f --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/classes/ApexController.cls @@ -0,0 +1,39 @@ +public class ApexController { + public Id AccountIdProp { get; set; } + + public ApexController() { + acc = [SELECT Id, Name, Site FROM Account + WHERE Id = :ApexPages.currentPage().getParameters().get('id')]; + this.AccountIdProp = acc.Id; + } + + public Id getAccountId() { + return acc.id; + } + + public String getAccountName() { + return acc.name; + } + + public InnerController getInnerController() { + return new InnerController(this); + } + + public class InnerController { + private ApexController parent; + public Id InnerAccountIdProp { get; set; } + + public InnerController(ApexController parent) { + this.parent = parent; + this.InnerAccountIdProp = parent.AccountIdProp; + } + + public Id getInnerAccountId() { + return 'Inner: ' + parent.acc.id; + } + + public String getInnerAccountName() { + return 'Inner: ' + parent.acc.name; + } + } +} \ No newline at end of file diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/classes/ApexExtension1.cls b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/classes/ApexExtension1.cls new file mode 100644 index 0000000000..764474cb46 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/classes/ApexExtension1.cls @@ -0,0 +1,4 @@ +public class ApexExtension1 { + public String StringFromExtension1 {get; set;} + public Id IdFromExtension1 {get; set;} +} diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/classes/ApexExtension2.cls b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/classes/ApexExtension2.cls new file mode 100644 index 0000000000..6f6ff84d65 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/classes/ApexExtension2.cls @@ -0,0 +1,4 @@ +public class ApexExtension2 { + public String StringFromExtension2 {get; set;} + public Id IdFromExtension2 {get; set;} +} diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/Checkbox__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/Checkbox__c.field-meta.xml new file mode 100644 index 0000000000..03ffc46229 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/Checkbox__c.field-meta.xml @@ -0,0 +1,9 @@ + + + Checkbox__c + false + false + + false + Checkbox + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/DateTime__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/DateTime__c.field-meta.xml new file mode 100644 index 0000000000..5e0ce1cd2c --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/DateTime__c.field-meta.xml @@ -0,0 +1,9 @@ + + + DateTime__c + false + + false + false + DateTime + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/LongTextArea__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/LongTextArea__c.field-meta.xml new file mode 100644 index 0000000000..6de4650b1f --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/LongTextArea__c.field-meta.xml @@ -0,0 +1,10 @@ + + + LongTextArea__c + false + + 32768 + false + LongTextArea + 3 + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/Picklist__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/Picklist__c.field-meta.xml new file mode 100644 index 0000000000..597d9d3d30 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/Picklist__c.field-meta.xml @@ -0,0 +1,24 @@ + + + Picklist__c + false + + false + false + Picklist + + + false + + Value1 + false + + + + Value2 + false + + + + + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/TextArea__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/TextArea__c.field-meta.xml new file mode 100644 index 0000000000..cd59083b9e --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/TextArea__c.field-meta.xml @@ -0,0 +1,9 @@ + + + TextArea__c + false + + false + false + TextArea + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/Text__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/Text__c.field-meta.xml new file mode 100644 index 0000000000..efeeb5262a --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/objects/Account/fields/Text__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Text__c + false + + 255 + false + false + Text + false + diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/pages/ApexController.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/pages/ApexController.page new file mode 100644 index 0000000000..2939c23f16 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/pages/ApexController.page @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/pages/StandardAccount.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/pages/StandardAccount.page new file mode 100644 index 0000000000..c95635785f --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/pages/StandardAccount.page @@ -0,0 +1,33 @@ + + + + {!Account.CreatedDate} + + {!Account.Checkbox__c} + + + + {!Account.DateTime__c} + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/pages/StandardAccountWithExtensions.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/pages/StandardAccountWithExtensions.page new file mode 100644 index 0000000000..0fcc494275 --- /dev/null +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeEl/metadata/sfdx/pages/StandardAccountWithExtensions.page @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pmd-xml/src/test/java/net/sourceforge/pmd/lang/xml/XmlParserOptionsTest.java b/pmd-xml/src/test/java/net/sourceforge/pmd/lang/xml/XmlParserOptionsTest.java index 7cfa36572b..547ea01bbe 100644 --- a/pmd-xml/src/test/java/net/sourceforge/pmd/lang/xml/XmlParserOptionsTest.java +++ b/pmd-xml/src/test/java/net/sourceforge/pmd/lang/xml/XmlParserOptionsTest.java @@ -4,7 +4,7 @@ package net.sourceforge.pmd.lang.xml; -import static net.sourceforge.pmd.lang.ParserOptionsTest.verifyOptionsEqualsHashcode; +import static net.sourceforge.pmd.lang.ParserOptionsTestUtils.verifyOptionsEqualsHashcode; import static net.sourceforge.pmd.util.CollectionUtil.listOf; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; diff --git a/pom.xml b/pom.xml index 001bd7afd4..4650ae96c4 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,7 @@ - 2020-10-24T08:17:24Z + 2020-12-12T08:42:10Z 8 @@ -719,7 +719,7 @@ org.codehaus.groovy groovy - 2.4.7 + 2.4.21