diff --git a/.ci/build.sh b/.ci/build.sh index d6475b70a1..f7651bff23 100755 --- a/.ci/build.sh +++ b/.ci/build.sh @@ -181,6 +181,10 @@ function pmd_ci_build_and_upload_doc() { pmd_code_removeJavadoc "${VERSION}-SNAPSHOT" # updating github release text + rm -f .bundle/config + bundle config set --local path vendor/bundle + bundle config set --local with release_notes_preprocessing + bundle install # renders, and skips the first 6 lines - the Jekyll front-matter local rendered_release_notes=$(bundle exec .ci/render_release_notes.rb docs/pages/release_notes.md | tail -n +6) local release_name="PMD ${VERSION} ($(date -u +%d-%B-%Y))" diff --git a/.ci/inc/maven-dependencies.inc b/.ci/inc/maven-dependencies.inc index 0aaa020b81..c99279b395 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-java8,!pmd-doc,!pmd-scala' + ./mvnw dependency:get -DgroupId=org.jetbrains.dokka \ -DartifactId=dokka-maven-plugin \ -Dversion=${dokka_version} \ diff --git a/.ci/inc/regression-tester.inc b/.ci/inc/regression-tester.inc index ffa9d25ee0..3bc72ccd81 100644 --- a/.ci/inc/regression-tester.inc +++ b/.ci/inc/regression-tester.inc @@ -33,93 +33,55 @@ function regression_tester_setup_ci() { } # -# Generate a new baseline and upload it to sourceforge -# -# Note: this function always succeeds, even if the upload fails. -# In that case, just a error logging is provided. +# Generate a new baseline and upload it to pmd-code.org # function regression_tester_uploadBaseline() { - log_debug "$FUNCNAME branch=${PMD_CI_BRANCH}" - local targetUrl="https://sourceforge.net/projects/pmd/files/pmd-regression-tester/" local pmdcodeUrl="https://pmd-code.org/pmd-regression-tester/" + local baseline_branch="${PMD_CI_BRANCH:-$PMD_CI_TAG}" + log_debug "$FUNCNAME branch=${baseline_branch}" - local errexitstate="$(shopt -po errexit)" - set +e # disable errexit - ( - # This handler is called if any command fails - function upload_failed() { - log_error "Error while uploading ${BRANCH_FILENAME}-baseline.zip to pmd-code.org!" - log_error "Please upload manually: ${pmdcodeUrl}" - #log_error "Error while uploading ${BRANCH_FILENAME}-baseline.zip to sourceforge!" - #log_error "Please upload manually: ${targetUrl}" - } - - # exit subshell after trap - set -e - trap upload_failed ERR - - log_info "Generating and uploading baseline for pmdtester..." - cd .. - bundle config --local gemfile pmd/Gemfile - bundle exec pmdtester \ - --mode single \ - --local-git-repo ./pmd \ - --patch-branch ${PMD_CI_BRANCH} \ - --patch-config ./pmd/.ci/files/all-java.xml \ - --list-of-project ./pmd/.ci/files/project-list.xml --html-flag \ - --error-recovery - cd target/reports - BRANCH_FILENAME="${PMD_CI_BRANCH/\//_}" - zip -q -r ${BRANCH_FILENAME}-baseline.zip ${BRANCH_FILENAME}/ - # ssh-key for pmd-code.org is setup already by pmd_ci_setup_ssh - scp ${BRANCH_FILENAME}-baseline.zip pmd@pmd-code.org:/httpdocs/pmd-regression-tester/ - log_success "Successfully uploaded ${BRANCH_FILENAME}-baseline.zip to ${pmdcodeUrl}" - #../../pmd/.ci/travis_wait "rsync -avh ${BRANCH_FILENAME}-baseline.zip ${PMD_SF_USER}@web.sourceforge.net:/home/frs/project/pmd/pmd-regression-tester/" - #log_success "Successfully uploaded ${BRANCH_FILENAME}-baseline.zip to ${targetUrl}" - ) - # restore errexit state - eval "$errexitstate" + log_info "Generating and uploading baseline for pmdtester (${baseline_branch})..." + pushd .. + rm -f .bundle/config + bundle config set --local gemfile pmd/Gemfile + bundle exec pmdtester \ + --mode single \ + --local-git-repo ./pmd \ + --patch-branch ${baseline_branch} \ + --patch-config ./pmd/.ci/files/all-java.xml \ + --list-of-project ./pmd/.ci/files/project-list.xml --html-flag \ + --error-recovery + pushd target/reports + BRANCH_FILENAME="${baseline_branch/\//_}" + zip -q -r ${BRANCH_FILENAME}-baseline.zip ${BRANCH_FILENAME}/ + # ssh-key for pmd-code.org is setup already by pmd_ci_setup_ssh + scp ${BRANCH_FILENAME}-baseline.zip pmd@pmd-code.org:/httpdocs/pmd-regression-tester/ + log_success "Successfully uploaded ${BRANCH_FILENAME}-baseline.zip to ${pmdcodeUrl}" + popd + popd } # # Execute danger, which executes pmd-regression-tester (via Dangerfile). # -# Note: this function always succeeds, even if the danger fails. -# In that case, just a error logging is provided. -# function regression_tester_executeDanger() { log_debug "$FUNCNAME" - local errexitstate="$(shopt -po errexit)" - set +e # disable errexit - ( - # This handler is called if any command fails - function danger_failed() { - log_error "Error while executing danger/pmd-regression-tester" - } + # Create a corresponding remote branch locally + if ! git show-ref --verify --quiet refs/heads/${PMD_CI_BRANCH}; then + git fetch --no-tags --depth=1 origin +refs/heads/${PMD_CI_BRANCH}:refs/remotes/origin/${PMD_CI_BRANCH} + git branch ${PMD_CI_BRANCH} origin/${PMD_CI_BRANCH} + log_debug "Created local branch ${PMD_CI_BRANCH}" + fi + # Fetch more commits of the PR for danger and regression tester + git fetch --no-tags --depth=50 origin +$(git rev-parse HEAD^2): + # Fetch more commits from master branch for regression tester + if [[ "${PMD_CI_BRANCH}" != "master" ]]; then + git fetch --no-tags --depth=50 origin +master: + git branch master origin/master + fi - # exit subshell after trap - set -e - trap danger_failed ERR - - # Create a corresponding remote branch locally - if ! git show-ref --verify --quiet refs/heads/${PMD_CI_BRANCH}; then - git fetch --no-tags --depth=1 origin +refs/heads/${PMD_CI_BRANCH}:refs/remotes/origin/${PMD_CI_BRANCH} - git branch ${PMD_CI_BRANCH} origin/${PMD_CI_BRANCH} - log_debug "Created local branch ${PMD_CI_BRANCH}" - fi - # Fetch more commits of the PR for danger and regression tester - git fetch --no-tags --depth=50 origin +$(git rev-parse HEAD^2): - # Fetch more commits from master branch for regression tester - if [[ "${PMD_CI_BRANCH}" != "master" ]]; then - git fetch --no-tags --depth=50 origin +master: - git branch master origin/master - fi - - log_info "Running danger on branch ${PMD_CI_BRANCH}" - bundle exec danger --verbose - log_success "Executing danger successfully" - ) - # restore errexit state - eval "$errexitstate" + log_info "Running danger on branch ${PMD_CI_BRANCH}" + bundle exec danger --verbose + log_success "Executed danger successfully" } 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/pull-requests.yml b/.github/workflows/pull-requests.yml index 169f000a80..9fd0c2fcbe 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -6,7 +6,7 @@ jobs: build: runs-on: ${{ matrix.os }} continue-on-error: false - timeout-minutes: 30 + timeout-minutes: 60 strategy: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] 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/CONTRIBUTING.md b/CONTRIBUTING.md index 372eab4e2d..0172b4acc9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,7 @@ There are various channels, on which you can ask questions: * On [StackOverflow](https://stackoverflow.com/questions/tagged/pmd): Make sure, to tag your question with "pmd". -* Create a issue for your question at . +* Create a new discussion for your question at . * Ask your question on Gitter . diff --git a/Dangerfile b/Dangerfile index 07cc07a715..1d51856dab 100644 --- a/Dangerfile +++ b/Dangerfile @@ -16,7 +16,7 @@ def run_pmdtester '--auto-gen-config', '--error-recovery', '--baseline-download-url', 'https://pmd-code.org/pmd-regression-tester/', - # '--debug', + #'--debug', ] begin @summary = PmdTester::Runner.new(argv).run @@ -39,7 +39,7 @@ def upload_report `tar -cf #{tar_filename} diff/` report_url = `curl -u #{ENV['PMD_CI_CHUNK_TOKEN']} -T #{tar_filename} https://chunk.io` if $?.success? - @logger.info "Successfully uploaded #{tar_filename} to chunk.io" + @logger.info "Successfully uploaded #{tar_filename} to #{report_url}" # set value of sticky to true and the message is kept after new commits are submitted to the PR message("This changeset " \ diff --git a/Gemfile b/Gemfile index b8861eb9c3..67b22b628d 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org/' # bleeding edge from git #gem 'pmdtester', :git => 'https://github.com/pmd/pmd-regression-tester.git' -gem 'pmdtester', '~> 1.1' +gem 'pmdtester', '~> 1' gem 'danger', '~> 5.6', '>= 5.6' # This group is only needed for rendering release notes diff --git a/Gemfile.lock b/Gemfile.lock index 9bdbcf639f..e742be01b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -31,36 +31,38 @@ GEM multipart-post (>= 1.2, < 3) faraday-http-cache (1.3.1) faraday (~> 0.8) - fugit (1.4.1) + fugit (1.4.2) et-orbi (~> 1.1, >= 1.1.8) raabro (~> 1.4) - git (1.7.0) + git (1.8.1) rchardet (~> 1.8) kramdown (1.17.0) - liquid (4.0.3) + liquid (5.0.0) logger-colors (1.0.0) - mini_portile2 (2.4.0) + mini_portile2 (2.5.0) multipart-post (2.1.1) nap (1.1.0) no_proxy_fix (0.1.2) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) - octokit (4.19.0) + nokogiri (1.11.1) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) + octokit (4.20.0) faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) open4 (1.3.4) - pmdtester (1.1.0) + pmdtester (1.1.1) differ (~> 0.1) liquid (>= 4.0) logger-colors (~> 1.0) - nokogiri (~> 1.8) + nokogiri (>= 1.11.0.rc4) rufus-scheduler (~> 3.5) slop (~> 4.6) public_suffix (4.0.6) raabro (1.4.0) + racc (1.5.2) rchardet (1.8.0) - rouge (3.25.0) - rufus-scheduler (3.6.0) + rouge (3.26.0) + rufus-scheduler (3.7.0) fugit (~> 1.1, >= 1.1.6) safe_yaml (1.0.5) sawyer (0.8.2) @@ -69,7 +71,7 @@ GEM slop (4.8.2) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - tzinfo (2.0.3) + tzinfo (2.0.4) concurrent-ruby (~> 1.0) unicode-display_width (1.7.0) @@ -79,7 +81,7 @@ PLATFORMS DEPENDENCIES danger (~> 5.6, >= 5.6) liquid (>= 4.0.0) - pmdtester (~> 1.1) + pmdtester (~> 1) rouge (>= 1.7, < 4) safe_yaml (>= 1.0) diff --git a/README.md b/README.md index d07be46f1f..f639615e8a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # PMD [![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) @@ -20,10 +20,12 @@ Objective-C, Perl, PHP, PLSQL, Python, Ruby, Salesforce.com Apex, Scala, Swift, ## Support -* How do I? -- Ask a question on [StackOverflow](https://stackoverflow.com/questions/tagged/pmd). -* I got this error, why? -- Ask a question on [StackOverflow](https://stackoverflow.com/questions/tagged/pmd). +* How do I? -- Ask a question on [StackOverflow](https://stackoverflow.com/questions/tagged/pmd) + or on [discussions](https://github.com/pmd/pmd/discussions). +* I got this error, why? -- Ask a question on [StackOverflow](https://stackoverflow.com/questions/tagged/pmd) + or on [discussions](https://github.com/pmd/pmd/discussions). * I got this error and I'm sure it's a bug -- file an [issue](https://github.com/pmd/pmd/issues). -* I have an idea/request/question -- file an [issue](https://github.com/pmd/pmd/issues). +* I have an idea/request/question -- create a new [discussion](https://github.com/pmd/pmd/discussions). * I have a quick question -- ask on our [Gitter chat](https://gitter.im/pmd/pmd). * Where's your documentation? -- diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 739f66ebe2..e474166447 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - activesupport (6.0.3.2) + activesupport (6.0.3.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -17,37 +17,40 @@ GEM commonmarker (0.17.13) ruby-enum (~> 0.5) concurrent-ruby (1.1.7) - dnsruby (1.61.4) + dnsruby (1.61.5) simpleidn (~> 0.1) - em-websocket (0.5.1) + em-websocket (0.5.2) eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) ethon (0.12.0) ffi (>= 1.3.0) eventmachine (1.2.7) execjs (2.7.0) - faraday (1.0.1) + faraday (1.3.0) + faraday-net_http (~> 1.0) multipart-post (>= 1.2, < 3) - ffi (1.13.1) + ruby2_keywords + faraday-net_http (1.0.0) + ffi (1.14.2) forwardable-extended (2.6.0) gemoji (3.0.1) - github-pages (207) + github-pages (209) github-pages-health-check (= 1.16.1) jekyll (= 3.9.0) jekyll-avatar (= 0.7.0) jekyll-coffeescript (= 1.1.1) jekyll-commonmark-ghpages (= 0.1.6) jekyll-default-layout (= 0.1.4) - jekyll-feed (= 0.13.0) + jekyll-feed (= 0.15.1) jekyll-gist (= 1.5.0) jekyll-github-metadata (= 2.13.0) - jekyll-mentions (= 1.5.1) + jekyll-mentions (= 1.6.0) jekyll-optional-front-matter (= 0.3.2) jekyll-paginate (= 1.1.0) jekyll-readme-index (= 0.3.0) - jekyll-redirect-from (= 0.15.0) + jekyll-redirect-from (= 0.16.0) jekyll-relative-links (= 0.6.1) - jekyll-remote-theme (= 0.4.1) + jekyll-remote-theme (= 0.4.2) jekyll-sass-converter (= 1.5.2) jekyll-seo-tag (= 2.6.1) jekyll-sitemap (= 1.4.0) @@ -55,7 +58,7 @@ GEM jekyll-theme-architect (= 0.1.1) jekyll-theme-cayman (= 0.1.1) jekyll-theme-dinky (= 0.1.1) - jekyll-theme-hacker (= 0.1.1) + jekyll-theme-hacker (= 0.1.2) jekyll-theme-leap-day (= 0.1.1) jekyll-theme-merlot (= 0.1.1) jekyll-theme-midnight (= 0.1.1) @@ -66,14 +69,14 @@ GEM jekyll-theme-tactile (= 0.1.1) jekyll-theme-time-machine (= 0.1.1) jekyll-titles-from-headings (= 0.5.3) - jemoji (= 0.11.1) + jemoji (= 0.12.0) kramdown (= 2.3.0) kramdown-parser-gfm (= 1.1.0) liquid (= 4.0.3) mercenary (~> 0.3) minima (= 2.5.1) nokogiri (>= 1.10.4, < 2.0) - rouge (= 3.19.0) + rouge (= 3.23.0) terminal-table (~> 1.4) github-pages-health-check (1.16.1) addressable (~> 2.3) @@ -114,14 +117,14 @@ GEM rouge (>= 2.0, < 4.0) jekyll-default-layout (0.1.4) jekyll (~> 3.0) - jekyll-feed (0.13.0) + jekyll-feed (0.15.1) jekyll (>= 3.7, < 5.0) jekyll-gist (1.5.0) octokit (~> 4.2) jekyll-github-metadata (2.13.0) jekyll (>= 3.4, < 5.0) octokit (~> 4.0, != 4.4.0) - jekyll-mentions (1.5.1) + jekyll-mentions (1.6.0) html-pipeline (~> 2.3) jekyll (>= 3.7, < 5.0) jekyll-optional-front-matter (0.3.2) @@ -129,14 +132,15 @@ GEM jekyll-paginate (1.1.0) jekyll-readme-index (0.3.0) jekyll (>= 3.0, < 5.0) - jekyll-redirect-from (0.15.0) + jekyll-redirect-from (0.16.0) jekyll (>= 3.3, < 5.0) jekyll-relative-links (0.6.1) jekyll (>= 3.3, < 5.0) - jekyll-remote-theme (0.4.1) + jekyll-remote-theme (0.4.2) addressable (~> 2.0) jekyll (>= 3.5, < 5.0) - rubyzip (>= 1.3.0) + jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) + rubyzip (>= 1.3.0, < 3.0) jekyll-sass-converter (1.5.2) sass (~> 3.4) jekyll-seo-tag (2.6.1) @@ -153,8 +157,8 @@ GEM jekyll-theme-dinky (0.1.1) jekyll (~> 3.5) jekyll-seo-tag (~> 2.0) - jekyll-theme-hacker (0.1.1) - jekyll (~> 3.5) + jekyll-theme-hacker (0.1.2) + jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-leap-day (0.1.1) jekyll (~> 3.5) @@ -188,7 +192,7 @@ GEM jekyll (>= 3.3, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) - jemoji (0.11.1) + jemoji (0.12.0) gemoji (~> 3.0) html-pipeline (~> 2.2) jekyll (>= 3.0, < 5.0) @@ -197,32 +201,35 @@ GEM kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) liquid (4.0.3) - listen (3.2.1) + listen (3.4.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) mercenary (0.3.6) - mini_portile2 (2.4.0) + mini_portile2 (2.5.0) minima (2.5.1) jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - minitest (5.14.1) + minitest (5.14.3) multipart-post (2.1.1) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) - octokit (4.18.0) + nokogiri (1.11.1) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) + octokit (4.20.0) faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (3.1.1) + racc (1.5.2) rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) rexml (3.2.4) - rouge (3.19.0) + rouge (3.23.0) ruby-enum (0.8.0) i18n + ruby2_keywords (0.0.2) rubyzip (2.3.0) safe_yaml (1.0.5) sass (3.7.4) @@ -240,13 +247,13 @@ GEM thread_safe (0.3.6) typhoeus (1.4.0) ethon (>= 0.9.0) - tzinfo (1.2.7) + tzinfo (1.2.9) thread_safe (~> 0.1) unf (0.1.4) unf_ext unf_ext (0.0.7.7) unicode-display_width (1.7.0) - zeitwerk (2.4.0) + zeitwerk (2.4.2) PLATFORMS ruby diff --git a/docs/_config.yml b/docs/_config.yml index ddc9c229f1..220abe44ad 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,9 +1,9 @@ repository: pmd/pmd pmd: - version: 6.30.0-SNAPSHOT - previous_version: 6.29.0 - date: ??-November-2020 + version: 6.31.0-SNAPSHOT + previous_version: 6.30.0 + date: ??-January-2021 release_type: minor # release types: major, minor, bugfix diff --git a/docs/pages/next_major_development.md b/docs/pages/next_major_development.md index 323c9757b9..94d7546cbd 100644 --- a/docs/pages/next_major_development.md +++ b/docs/pages/next_major_development.md @@ -125,6 +125,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::RulesetsFactoryUtils %} have been deprecated in favor of {% jdoc core::RuleSetLoader %}. This is easier to configure, and more maintainable than the multiple overloads of `RulesetsFactoryUtils`. +* 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. diff --git a/docs/pages/pmd/about/help.md b/docs/pages/pmd/about/help.md index c20f9fd066..8e45c96b8b 100644 --- a/docs/pages/pmd/about/help.md +++ b/docs/pages/pmd/about/help.md @@ -1,8 +1,8 @@ --- title: Getting Help permalink: pmd_about_help.html -author: Andreas Dangel -last_updated: September 2017 +author: Andreas Dangel +last_updated: January 2021 --- There are numerous ways of getting help: @@ -13,7 +13,7 @@ There are numerous ways of getting help: * If you found a bug, please create a new [github issue](https://github.com/pmd/pmd/issues). -* You can also ask questions in our [sourceforge forum](https://sourceforge.net/p/pmd/discussion/). +* You can also ask questions on [github discussions](https://github.com/pmd/pmd/discussions). * Or you can join the [Mailing List](https://lists.sourceforge.net/lists/listinfo/pmd-devel) or browse through the archives ([archive1](http://java-pmd.30631.n5.nabble.com/), [archive2](http://web.archive.org/web/20160715035623/http://blog.gmane.org:80/gmane.comp.java.audit.pmd.devel)). 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 52dd9dd3b6..1e8ce00b4e 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -14,40 +14,24 @@ This is a {{ site.pmd.release_type }} release. ### New and noteworthy -##### CPD +#### New Rules -* 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) +* The new Apex rule {% rule "apex/errorprone/OverrideBothEqualsAndHashcode" %} brings the well known Java rule + to Apex. In Apex the same principle applies: `equals` and `hashCode` should always be overridden + together to ensure collection classes such as Maps and Sets work as expected. ### 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 - + * [#2970](https://github.com/pmd/pmd/issues/2970): \[core] PMD 6.30.0 release is not reproducible + * [#2994](https://github.com/pmd/pmd/pull/2994): \[core] Fix code climate severity strings +* java-bestpractices + * [#575](https://github.com/pmd/pmd/issues/575): \[java] LiteralsFirstInComparisons should consider constant fields +* java-codestyle + * [#2960](https://github.com/pmd/pmd/issues/2960): \[java] Thread issue in MethodNamingConventionsRule ### API Changes -#### Deprecated APIs - -* {% 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) - #### Experimental APIs * The method {% jdoc !!core::lang.ast.GenericToken#getKind() %} has been added as experimental. This @@ -56,27 +40,14 @@ This is a {{ site.pmd.release_type }} release. returned constant depends on the actual language and might change whenever the grammar of the language is changed. -#### Internal APIs - -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) +* [#2666](https://github.com/pmd/pmd/pull/2666): \[swift] Manage swift5 string literals - [kenji21](https://github.com/kenji21) +* [#2959](https://github.com/pmd/pmd/pull/2959): \[apex] New Rule: override equals and hashcode rule - [recdevs](https://github.com/recdevs) +* [#2964](https://github.com/pmd/pmd/pull/2964): \[cs] Update C# grammar for additional C# 7 and C# 8 features - [Maikel Steneker](https://github.com/maikelsteneker) +* [#2965](https://github.com/pmd/pmd/pull/2965): \[cs] Improvements for ignore sequences of literals functionality - [Maikel Steneker](https://github.com/maikelsteneker) +* [#2983](https://github.com/pmd/pmd/pull/2983): \[java] LiteralsFirstInComparisons should consider constant fields - [Ozan Gulle](https://github.com/ozangulle) +* [#2994](https://github.com/pmd/pmd/pull/2994): \[core] Fix code climate severity strings - [Vincent Maurin](https://github.com/vmaurin) {% endtocmaker %} + diff --git a/docs/pages/release_notes_old.md b/docs/pages/release_notes_old.md index a02971b7c0..85489a2b5e 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 RulesetsFactoryUtils have been deprecated in favor of RuleSetLoader. This is easier to configure, and more maintainable than the multiple overloads of `RulesetsFactoryUtils`. +* 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-jorje/pom.xml b/pmd-apex-jorje/pom.xml index 9f97ef4389..e751b1785e 100644 --- a/pmd-apex-jorje/pom.xml +++ b/pmd-apex-jorje/pom.xml @@ -8,7 +8,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-apex/pom.xml b/pmd-apex/pom.xml index cb2ae54b65..268aa9ec98 100644 --- a/pmd-apex/pom.xml +++ b/pmd-apex/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/errorprone/OverrideBothEqualsAndHashcodeRule.java b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/errorprone/OverrideBothEqualsAndHashcodeRule.java new file mode 100644 index 0000000000..29d90ad523 --- /dev/null +++ b/pmd-apex/src/main/java/net/sourceforge/pmd/lang/apex/rule/errorprone/OverrideBothEqualsAndHashcodeRule.java @@ -0,0 +1,68 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.rule.errorprone; + +import net.sourceforge.pmd.lang.apex.ast.ASTMethod; +import net.sourceforge.pmd.lang.apex.ast.ASTParameter; +import net.sourceforge.pmd.lang.apex.ast.ASTUserClass; +import net.sourceforge.pmd.lang.apex.ast.ApexNode; +import net.sourceforge.pmd.lang.apex.rule.AbstractApexRule; + +public class OverrideBothEqualsAndHashcodeRule extends AbstractApexRule { + + public OverrideBothEqualsAndHashcodeRule() { + addRuleChainVisit(ASTUserClass.class); + } + + @Override + public Object visit(ASTUserClass node, Object data) { + ApexNode equalsNode = null; + ApexNode hashNode = null; + for (ASTMethod method : node.findChildrenOfType(ASTMethod.class)) { + if (equalsNode == null && isEquals(method)) { + equalsNode = method; + } + if (hashNode == null && isHashCode(method)) { + hashNode = method; + } + if (hashNode != null && equalsNode != null) { + break; + } + } + + if (equalsNode != null && hashNode == null) { + addViolation(data, equalsNode); + } else if (hashNode != null && equalsNode == null) { + addViolation(data, hashNode); + } + + return data; + } + + private boolean isEquals(ASTMethod node) { + int numParams = 0; + String paramType = null; + for (int ix = 0; ix < node.getNumChildren(); ix++) { + ApexNode sn = node.getChild(ix); + if (sn instanceof ASTParameter) { + numParams++; + paramType = ((ASTParameter) sn).getType(); + } + } + return numParams == 1 && "equals".equalsIgnoreCase(node.getImage()) && "Object".equalsIgnoreCase(paramType); + } + + private boolean isHashCode(ASTMethod node) { + int numParams = 0; + for (int ix = 0; ix < node.getNumChildren(); ix++) { + ApexNode sn = node.getChild(ix); + if (sn instanceof ASTParameter) { + numParams++; + } + } + + return numParams == 0 && "hashCode".equalsIgnoreCase(node.getImage()); + } +} diff --git a/pmd-apex/src/main/resources/category/apex/errorprone.xml b/pmd-apex/src/main/resources/category/apex/errorprone.xml index 24d5483197..66779d65b6 100644 --- a/pmd-apex/src/main/resources/category/apex/errorprone.xml +++ b/pmd-apex/src/main/resources/category/apex/errorprone.xml @@ -111,6 +111,30 @@ public without sharing class Foo { + + + Apex supported non existent annotations for legacy reasons. + In the future, use of such non-existent annotations could result in broken apex code that will not compile. + This will prevent users of garbage annotations from being able to use legitimate annotations added to Apex in the future. + A full list of supported annotations can be found at https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_annotation.htm + + 3 + + + + + - + since="6.31.0" + message="Ensure you override both equals() and hashCode()" + class="net.sourceforge.pmd.lang.apex.rule.errorprone.OverrideBothEqualsAndHashcodeRule" + externalInfoUrl="${pmd.website.baseurl}/pmd_rules_apex_errorprone.html#overridebothequalsandhashcode"> - Apex supported non existent annotations for legacy reasons. - In the future, use of such non-existent annotations could result in broken apex code that will not compile. - This will prevent users of garbage annotations from being able to use legitimate annotations added to Apex in the future. - A full list of supported annotations can be found at https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_annotation.htm +Override both `public Boolean equals(Object obj)`, and `public Integer hashCode()`, or override neither. +Even if you are inheriting a hashCode() from a parent class, consider implementing hashCode and explicitly +delegating to your superclass. + +This is especially important when [Using Custom Types in Map Keys and Sets](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/langCon_apex_collections_maps_keys_userdefined.htm). 3 - diff --git a/pmd-apex/src/main/resources/rulesets/apex/quickstart.xml b/pmd-apex/src/main/resources/rulesets/apex/quickstart.xml index 10b0ddcfcc..c0842d8641 100644 --- a/pmd-apex/src/main/resources/rulesets/apex/quickstart.xml +++ b/pmd-apex/src/main/resources/rulesets/apex/quickstart.xml @@ -472,5 +472,6 @@ + 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 11ef33e0d1..e8f53a6d68 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 @@ -14,16 +14,15 @@ import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.SystemErrRule; -import net.sourceforge.pmd.RulePriority; import net.sourceforge.pmd.RuleSet; import net.sourceforge.pmd.RuleSetFactory; -import net.sourceforge.pmd.RulesetsFactoryUtils; +import net.sourceforge.pmd.RuleSetLoader; public class DefaultRulesetTest { @Rule public final SystemErrRule systemErrRule = new SystemErrRule().enableLog().muteForSuccessfulTests(); - private RuleSetFactory factory = RulesetsFactoryUtils.createFactory(RulePriority.LOW, true, false); + private RuleSetFactory factory = new RuleSetLoader().enableCompatibility(false).toFactory(); @Test public void loadDefaultRuleset() throws Exception { diff --git a/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/rule/errorprone/OverrideBothEqualsAndHashcodeTest.java b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/rule/errorprone/OverrideBothEqualsAndHashcodeTest.java new file mode 100644 index 0000000000..0b334a5f9f --- /dev/null +++ b/pmd-apex/src/test/java/net/sourceforge/pmd/lang/apex/rule/errorprone/OverrideBothEqualsAndHashcodeTest.java @@ -0,0 +1,11 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.apex.rule.errorprone; + +import net.sourceforge.pmd.testframework.PmdRuleTst; + +public class OverrideBothEqualsAndHashcodeTest extends PmdRuleTst { + // no additional unit tests +} diff --git a/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/errorprone/xml/OverrideBothEqualsAndHashcode.xml b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/errorprone/xml/OverrideBothEqualsAndHashcode.xml new file mode 100644 index 0000000000..d321c92777 --- /dev/null +++ b/pmd-apex/src/test/resources/net/sourceforge/pmd/lang/apex/rule/errorprone/xml/OverrideBothEqualsAndHashcode.xml @@ -0,0 +1,166 @@ + + + + + hash code only + 1 + + + + + nested hash code only + 1 + + + + + equals only + 1 + + + + + nested equals only, checking case insensitiveness + 1 + + + + + overrides both + 0 + + + + + nested overrides both + 0 + + + + + overrides neither + 0 + + + + + equals sig uses String, not Object + 1 + + + + + interface + 0 + + + + + implements equals but with 2 args, hashCode overloaded as well + 0 + + + + + overloaded hashCode + 0 + + + + + overloaded both + 0 + + + + + overloaded hashCode, should fail on equals + 1 + + + + + implements hashCode but with args + 0 + a) { + return 0; + } +} + ]]> + + diff --git a/pmd-core/pom.xml b/pmd-core/pom.xml index 1046e15412..a313f1c309 100644 --- a/pmd-core/pom.xml +++ b/pmd-core/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-core/src/main/ant/alljavacc.xml b/pmd-core/src/main/ant/alljavacc.xml index 2940d3ee58..ff3cb30801 100644 --- a/pmd-core/src/main/ant/alljavacc.xml +++ b/pmd-core/src/main/ant/alljavacc.xml @@ -30,15 +30,23 @@ Using JavaCC home: ${javacc-home.path} - + + + + + - + + + + + + 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 066d2fc935..88e66e2d53 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/PMD.java @@ -11,16 +11,18 @@ import java.io.Writer; import java.net.URISyntaxException; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.ConsoleHandler; import java.util.logging.Level; import java.util.logging.Logger; +import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.benchmark.TextTimingReportRenderer; import net.sourceforge.pmd.benchmark.TimeTracker; import net.sourceforge.pmd.benchmark.TimedOperation; @@ -41,11 +43,9 @@ import net.sourceforge.pmd.processor.AbstractPMDProcessor; import net.sourceforge.pmd.processor.MonoThreadProcessor; import net.sourceforge.pmd.processor.MultiThreadProcessor; import net.sourceforge.pmd.renderers.Renderer; -import net.sourceforge.pmd.stat.Metric; import net.sourceforge.pmd.util.ClasspathClassLoader; import net.sourceforge.pmd.util.FileUtil; import net.sourceforge.pmd.util.IOUtil; -import net.sourceforge.pmd.util.ResourceLoader; import net.sourceforge.pmd.util.database.DBMSMetadata; import net.sourceforge.pmd.util.database.DBURI; import net.sourceforge.pmd.util.database.SourceObject; @@ -89,7 +89,10 @@ public class PMD { /** * Create a PMD instance using a default Configuration. Changes to the * configuration may be required. + * + * @deprecated Just use the static methods, and maintain your {@link PMDConfiguration} separately. */ + @Deprecated public PMD() { this(new PMDConfiguration()); } @@ -97,9 +100,11 @@ public class PMD { /** * Create a PMD instance using the specified Configuration. * - * @param configuration - * The runtime Configuration of PMD to use. + * @param configuration The runtime Configuration of PMD to use. + * + * @deprecated Just use the static methods, and maintain your {@link PMDConfiguration} separately. */ + @Deprecated public PMD(PMDConfiguration configuration) { this.configuration = configuration; this.rulesetsFileProcessor = new SourceCodeProcessor(configuration); @@ -115,7 +120,10 @@ public class PMD { * @throws PMDException * if the URI couldn't be parsed * @see DBURI + * + * @deprecated Will be hidden as part of the parsing of {@link PMD#getApplicableFiles(PMDConfiguration, Set)} */ + @Deprecated public static List getURIDataSources(String uriString) throws PMDException { List dataSources = new ArrayList<>(); @@ -161,7 +169,11 @@ public class PMD { * @param configuration * the given configuration * @return the pre-configured parser + * + * @deprecated This is internal */ + @Deprecated + @InternalApi public static Parser parserFor(LanguageVersion languageVersion, PMDConfiguration configuration) { // TODO Handle Rules having different parser options. @@ -179,7 +191,10 @@ public class PMD { * * @return The configuration. * @see PMDConfiguration + * + * @deprecated Don't create a PMD instance just to create a {@link PMDConfiguration} */ + @Deprecated public PMDConfiguration getConfiguration() { return configuration; } @@ -188,7 +203,9 @@ public class PMD { * Gets the source code processor. * * @return SourceCodeProcessor + * @deprecated Source code processor is internal */ + @Deprecated public SourceCodeProcessor getSourceCodeProcessor() { return rulesetsFileProcessor; } @@ -203,7 +220,7 @@ public class PMD { public static int doPMD(PMDConfiguration configuration) { // Load the RuleSets - final RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.getRulesetFactory(configuration, new ResourceLoader()); + final RuleSetFactory ruleSetFactory = RuleSetLoader.fromPmdConfig(configuration).toFactory(); final RuleSets ruleSets = RulesetsFactoryUtils.getRuleSetsWithBenchmark(configuration.getRuleSets(), ruleSetFactory); if (ruleSets == null) { return PMDCommandLineInterface.NO_ERRORS_STATUS; @@ -221,28 +238,15 @@ public class PMD { renderer.start(); } - RuleContext ctx = new RuleContext(); - final AtomicInteger violations = new AtomicInteger(0); - ctx.getReport().addListener(new ThreadSafeReportListener() { - @Override - public void ruleViolationAdded(RuleViolation ruleViolation) { - violations.getAndIncrement(); - } - - @Override - public void metricAdded(Metric metric) { - // ignored - not needed for counting violations - } - }); - + Report report; try (TimedOperation to = TimeTracker.startOperation(TimedOperationCategory.FILE_PROCESSING)) { - processFiles(configuration, ruleSetFactory, files, ctx, renderers); + report = processFiles(configuration, Arrays.asList(ruleSets.getAllRuleSets()), files, renderers); } try (TimedOperation rto = TimeTracker.startOperation(TimedOperationCategory.REPORTING)) { renderer.end(); renderer.flush(); - return violations.get(); + return report.getViolations().size(); } } catch (Exception e) { String message = e.getMessage(); @@ -274,7 +278,10 @@ public class PMD { * @param sourceCodeFile * the source code file * @return the rule context + * + * @deprecated Not useful */ + @Deprecated public static RuleContext newRuleContext(String sourceCodeFilename, File sourceCodeFile) { RuleContext context = new RuleContext(); @@ -297,19 +304,60 @@ public class PMD { * 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) { + 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 = new RuleSetFactory(ruleSetFactory, false); + 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. + * + * @param configuration Configuration for the run. Note that the files, + * and rulesets, are ignored, as they are supplied + * as parameters + * @param rulesets Parsed rulesets + * @param files Files to process, will be closed by this method. + * @param renderers Renderers that render the report + * + * @return Report in which violations are accumulated + * + * @throws RuntimeException If processing fails + */ + public static Report processFiles(final PMDConfiguration configuration, + final List rulesets, + final Collection files, + final List renderers) { + encourageToUseIncrementalAnalysis(configuration); + Report report = new Report(); + report.addListener(configuration.getAnalysisCache()); + + List sortedFiles = new ArrayList<>(files); + sortFiles(configuration, sortedFiles); + + RuleContext ctx = new RuleContext(); + ctx.setReport(report); + newFileProcessor(configuration).processFiles(new RuleSets(rulesets), sortedFiles, ctx, renderers); + configuration.getAnalysisCache().persist(); + return report; + } + private static void sortFiles(final PMDConfiguration configuration, final List files) { if (configuration.isStressTest()) { // randomize processing order @@ -442,7 +490,7 @@ public class PMD { } /** - * Entry to invoke PMD as command line tool + * Entry to invoke PMD as command line tool. Note that this will invoke {@link System#exit(int)}. * * @param args * command line arguments @@ -452,7 +500,8 @@ public class PMD { } /** - * Parses the command line arguments and executes PMD. + * Parses the command line arguments and executes PMD. Returns the + * exit code without exiting the VM. * * @param args * command line arguments diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/PMDException.java b/pmd-core/src/main/java/net/sourceforge/pmd/PMDException.java index 4cadd9728d..d2ab5fc2b3 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/PMDException.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/PMDException.java @@ -13,6 +13,7 @@ package net.sourceforge.pmd; * @version $Revision$, $Date$ * @since August 30, 2002 */ +@Deprecated public class PMDException extends Exception { private static final long serialVersionUID = 6938647389367956874L; 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 f0a75a7028..8bafa2ccc5 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/Report.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/Report.java @@ -47,6 +47,7 @@ public class Report implements Iterable { private final List listeners = new ArrayList<>(); private final List errors = new ArrayList<>(); private final List configErrors = new ArrayList<>(); + private final Object lock = new Object(); private Map linesToSuppress = new HashMap<>(); private long start; private long end; @@ -394,20 +395,25 @@ public class Report implements Iterable { * summary over all violations is needed as PMD creates one report per file * by default. * - * @param r - * the report to be merged into this. + *

This is synchronized on an internal lock (note that other mutation + * operations are not synchronized, todo for pmd 7). + * + * @param r the report to be merged into this. + * * @see AbstractAccumulatingRenderer */ public void merge(Report r) { - errors.addAll(r.errors); - configErrors.addAll(r.configErrors); - metrics.addAll(r.metrics); - suppressedRuleViolations.addAll(r.suppressedRuleViolations); + synchronized (lock) { + errors.addAll(r.errors); + configErrors.addAll(r.configErrors); + metrics.addAll(r.metrics); + 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); - violationTree.addRuleViolation(violation); + for (RuleViolation violation : r.getViolations()) { + int index = Collections.binarySearch(violations, violation, RuleViolation.DEFAULT_COMPARATOR); + violations.add(index < 0 ? -index - 1 : index, violation); + violationTree.addRuleViolation(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 c22906166a..f53033e5dc 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSet.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSet.java @@ -28,6 +28,7 @@ import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.rule.RuleReference; +import net.sourceforge.pmd.lang.rule.XPathRule; import net.sourceforge.pmd.util.filter.Filter; import net.sourceforge.pmd.util.filter.Filters; @@ -100,6 +101,89 @@ public class RuleSet implements ChecksumAware { filter = rs.filter; // filters are immutable, can be shared } + /** + * Creates a new ruleset containing a single rule. The ruleset will + * have default description, name, and null file name. + * + * @param rule The rule being created + * + * @return The newly created RuleSet + */ + public static RuleSet forSingleRule(final Rule rule) { + final long checksum; + if (rule instanceof XPathRule) { + checksum = ((XPathRule) rule).getXPathExpression().hashCode(); + } else { + // TODO : Is this good enough? all properties' values + rule name + checksum = rule.getPropertiesByPropertyDescriptor().values().hashCode() * 31 + rule.getName().hashCode(); + } + + final RuleSetBuilder builder = + new RuleSetBuilder(checksum) + .withName(rule.getName()) + .withDescription("RuleSet for " + rule.getName()); + builder.addRule(rule); + 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 + * @param includePatterns list of include patterns, that override the exclude patterns + * @param rules the collection with the rules to add to the new ruleset + * + * @return the new ruleset + * + * @throws NullPointerException If any parameter is null, or the collections contain null elements + */ + public static RuleSet create(String name, + String description, + String fileName, + Collection excludePatterns, + Collection includePatterns, + Iterable rules) { + RuleSetBuilder builder = new RuleSetBuilder(0L); // TODO: checksum missing + builder.withName(name) + .withDescription(description) + .withFileName(fileName) + .replaceFileExclusions(excludePatterns) + .replaceFileInclusions(includePatterns); + for (Rule rule : rules) { + builder.addRule(rule); + } + return builder.build(); + } + + /** + * 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 + */ + public static RuleSet copy(RuleSet original) { + return new RuleSet(original); + } + /* package */ static class RuleSetBuilder { public String description; 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 3fef741d9a..efd2b0ab31 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactory.java @@ -13,7 +13,6 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -34,22 +33,20 @@ 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.MockRule; import net.sourceforge.pmd.lang.rule.RuleReference; -import net.sourceforge.pmd.lang.rule.XPathRule; import net.sourceforge.pmd.rules.RuleFactory; import net.sourceforge.pmd.util.ResourceLoader; /** * RuleSetFactory is responsible for creating RuleSet instances from XML - * content. By default Rules will be loaded using the {@link RulePriority#LOW} priority, - * with Rule deprecation warnings off; - * the ruleset compatibility filter is active, too (see {@link RuleSetFactoryCompatibility}); - * if the ruleset contains rule references (e.g. for renamed or moved rules), these - * are ignored by default. + * 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 { private static final Logger LOG = Logger.getLogger(RuleSetFactory.class.getName()); @@ -67,7 +64,7 @@ public class RuleSetFactory { private final Map parsedRulesets = new HashMap<>(); /** - * @deprecated Use {@link RulesetsFactoryUtils#defaultFactory()} + * @deprecated Use a {@link RuleSetLoader} to build a new factory */ @Deprecated // to be removed with PMD 7.0.0. public RuleSetFactory() { @@ -75,8 +72,7 @@ public class RuleSetFactory { } /** - * @deprecated Use {@link RulesetsFactoryUtils#createFactory(ClassLoader, RulePriority, boolean, boolean)} - * or {@link RulesetsFactoryUtils#createFactory(RulePriority, boolean, boolean)} + * @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, @@ -85,8 +81,7 @@ public class RuleSetFactory { } /** - * @deprecated Use {@link RulesetsFactoryUtils#createFactory(ClassLoader, RulePriority, boolean, boolean)} - * or {@link RulesetsFactoryUtils#createFactory(RulePriority, boolean, boolean)} + * @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, @@ -111,16 +106,18 @@ public class RuleSetFactory { /** * 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. + * @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. @@ -138,28 +135,12 @@ public class RuleSetFactory { * @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 { - String rulesetsProperties = null; - List ruleSetReferenceIds = new ArrayList<>(); - for (Language language : LanguageRegistry.findWithRuleSupport()) { - Properties props = new Properties(); - rulesetsProperties = "category/" + language.getTerseName() + "/categories.properties"; - try (InputStream inputStream = resourceLoader.loadClassPathResourceAsStreamOrThrow(rulesetsProperties)) { - props.load(inputStream); - String rulesetFilenames = props.getProperty("rulesets.filenames"); - if (rulesetFilenames != null) { - ruleSetReferenceIds.addAll(RuleSetReferenceId.parse(rulesetFilenames)); - } - } catch (RuleSetNotFoundException e) { - LOG.warning("The language " + language.getTerseName() + " provides no " + rulesetsProperties + "."); - } catch (IOException ioe) { - 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")); - } - } - return createRuleSets(ruleSetReferenceIds).getRuleSetsIterator(); + return toLoader().getStandardRuleSets().iterator(); } /** @@ -174,7 +155,11 @@ public class RuleSetFactory { * @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)); } @@ -188,7 +173,10 @@ public class RuleSetFactory { * @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 { RuleSets ruleSets = new RuleSets(); for (RuleSetReferenceId ruleSetReferenceId : ruleSetReferenceIds) { @@ -210,7 +198,10 @@ public class RuleSetFactory { * @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()) { @@ -229,7 +220,10 @@ public class RuleSetFactory { * @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 { return createRuleSet(ruleSetReferenceId, includeDeprecatedRuleReferences); } @@ -250,7 +244,10 @@ public class RuleSetFactory { * * @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(); @@ -274,23 +271,17 @@ public class RuleSetFactory { * @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) { - RuleSetBuilder builder = new RuleSetBuilder(0L); // TODO: checksum missing - builder.withName(name) - .withDescription(description) - .withFileName(fileName) - .replaceFileExclusions(toPatterns(excludePatterns)) - .replaceFileInclusions(toPatterns(includePatterns)); - for (Rule rule : rules) { - builder.addRule(rule); - } - return builder.build(); + return RuleSet.create(name, description, fileName, toPatterns(excludePatterns), toPatterns(includePatterns), rules); } private Collection toPatterns(Collection sources) { @@ -308,24 +299,15 @@ public class RuleSetFactory { /** * Creates a new RuleSet containing a single rule. * - * @param rule - * The rule being created + * @param rule The rule being created + * * @return The newly created RuleSet + * + * @deprecated Use {@link RuleSet#forSingleRule(Rule)} */ - public RuleSet createSingleRuleRuleSet(final Rule rule) { // TODO make static? - final long checksum; - if (rule instanceof XPathRule) { - checksum = ((XPathRule) rule).getXPathExpression().hashCode(); - } else { - // TODO : Is this good enough? all properties' values + rule name - checksum = rule.getPropertiesByPropertyDescriptor().values().hashCode() * 31 + rule.getName().hashCode(); - } - - final RuleSetBuilder builder = new RuleSetBuilder(checksum) - .withName(rule.getName()) - .withDescription("RuleSet for " + rule.getName()); - builder.addRule(rule); - return builder.build(); + @Deprecated + public RuleSet createSingleRuleRuleSet(final Rule rule) { + return RuleSet.forSingleRule(rule); } /** @@ -348,7 +330,8 @@ public class RuleSetFactory { private Rule createRule(RuleSetReferenceId ruleSetReferenceId, boolean withDeprecatedRuleReferences) throws RuleSetNotFoundException { if (ruleSetReferenceId.isAllRules()) { - throw new IllegalArgumentException("Cannot parse a single Rule from an all Rule RuleSet reference: <" + ruleSetReferenceId + ">."); + throw new IllegalArgumentException( + "Cannot parse a single Rule from an all Rule RuleSet reference: <" + ruleSetReferenceId + ">."); } RuleSet ruleSet; // java8: computeIfAbsent @@ -438,9 +421,6 @@ public class RuleSetFactory { ruleSetBuilder.filterRulesByPriority(minimumPriority); return ruleSetBuilder.build(); - } catch (ReflectiveOperationException ex) { - ex.printStackTrace(); - throw new RuntimeException("Couldn't find the class " + ex.getMessage(), ex); } catch (ParserConfigurationException | IOException | SAXException ex) { ex.printStackTrace(); throw new RuntimeException("Couldn't read the ruleset " + ruleSetReferenceId + ": " + ex.getMessage(), ex); @@ -512,7 +492,7 @@ public class RuleSetFactory { */ private void parseRuleNode(RuleSetReferenceId ruleSetReferenceId, RuleSetBuilder ruleSetBuilder, Node ruleNode, boolean withDeprecatedRuleReferences, Set rulesetReferences) - throws ClassNotFoundException, InstantiationException, IllegalAccessException, RuleSetNotFoundException { + throws RuleSetNotFoundException { Element ruleElement = (Element) ruleNode; String ref = ruleElement.getAttribute("ref"); if (ref.endsWith("xml")) { @@ -557,7 +537,7 @@ public class RuleSetFactory { // load the ruleset with minimum priority low, so that we get all rules, to be able to exclude any rule // minimum priority will be applied again, before constructing the final ruleset - RuleSetFactory ruleSetFactory = new RuleSetFactory(resourceLoader, RulePriority.LOW, false, this.compatibilityFilter != null); + RuleSetFactory ruleSetFactory = toLoader().filterAbovePriority(RulePriority.LOW).warnDeprecated(false).toFactory(); RuleSet otherRuleSet = ruleSetFactory.createRuleSet(RuleSetReferenceId.parse(ref).get(0)); List potentialRules = new ArrayList<>(); int countDeprecated = 0; @@ -620,7 +600,7 @@ public class RuleSetFactory { * Must be a rule element node. */ private void parseSingleRuleNode(RuleSetReferenceId ruleSetReferenceId, RuleSetBuilder ruleSetBuilder, - Node ruleNode) throws ClassNotFoundException, InstantiationException, IllegalAccessException { + Node ruleNode) { Element ruleElement = (Element) ruleNode; // Stop if we're looking for a particular Rule, and this element is not @@ -672,7 +652,7 @@ public class RuleSetFactory { // load the ruleset with minimum priority low, so that we get all rules, to be able to exclude any rule // minimum priority will be applied again, before constructing the final ruleset - RuleSetFactory ruleSetFactory = new RuleSetFactory(resourceLoader, RulePriority.LOW, false, this.compatibilityFilter != null); + RuleSetFactory ruleSetFactory = toLoader().filterAbovePriority(RulePriority.LOW).warnDeprecated(false).toFactory(); boolean isSameRuleSet = false; RuleSetReferenceId otherRuleSetReferenceId = RuleSetReferenceId.parse(ref).get(0); @@ -832,4 +812,19 @@ public class RuleSetFactory { return false; } } + + + /** + * Create a new {@link RuleSetLoader} with the same config as this + * factory. This is a transitional API. + */ + public RuleSetLoader toLoader() { + return new RuleSetLoader().loadResourcesWith(resourceLoader) + .filterAbovePriority(minimumPriority) + .warnDeprecated(warnDeprecated) + .enableCompatibility(compatibilityFilter != null) + .includeDeprecatedRuleReferences(includeDeprecatedRuleReferences); + } + + } 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 cd933218fd..6ed1d189a4 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetFactoryCompatibility.java @@ -18,13 +18,20 @@ import java.util.regex.Pattern; import org.apache.commons.io.IOUtils; +import net.sourceforge.pmd.annotation.InternalApi; + /** * Provides a simple filter mechanism to avoid failing to parse an old ruleset, * which references rules, that have either been removed from PMD already or * 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()); diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoadException.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoadException.java new file mode 100644 index 0000000000..a59321e146 --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoadException.java @@ -0,0 +1,32 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd; + +import net.sourceforge.pmd.annotation.InternalApi; + +/** + * An exception that is thrown when something wrong occurs while + * {@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 { + + /** Constructors are internal. */ + @InternalApi + public RuleSetLoadException(String message, Throwable cause) { + super(message, cause); + } + + /** Constructors are internal. */ + @InternalApi + 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 new file mode 100644 index 0000000000..61f441db7e --- /dev/null +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetLoader.java @@ -0,0 +1,230 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +import net.sourceforge.pmd.lang.Language; +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.util.CollectionUtil; +import net.sourceforge.pmd.util.ResourceLoader; + +/** + * Configurable object to load rulesets from XML resources. + * This can be configured using a fluent API, see eg {@link #warnDeprecated(boolean)}. + * To create a new ruleset, use {@link #loadFromResource(String)} + * or some such overload. + */ +public final class RuleSetLoader { + + private static final Logger LOG = 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 boolean includeDeprecatedRuleReferences = false; + + /** + * Specify that the given classloader should be used to resolve + * paths to external ruleset references. The default uses PMD's + * own classpath. + */ + public RuleSetLoader loadResourcesWith(ClassLoader classLoader) { + this.resourceLoader = new ResourceLoader(classLoader); + return this; + } + + // internal + RuleSetLoader loadResourcesWith(ResourceLoader loader) { + this.resourceLoader = loader; + return this; + } + + /** + * Filter loaded rules to only those that match or are above + * the given priority. The default is {@link RulePriority#LOW}, + * ie, no filtering occurs. + * + * @return This instance, modified + */ + public RuleSetLoader filterAbovePriority(RulePriority minimumPriority) { + this.minimumPriority = minimumPriority; + return this; + } + + /** + * Log a warning when referencing a deprecated rule. + * This is enabled by default. + * + * @return This instance, modified + */ + public RuleSetLoader warnDeprecated(boolean warn) { + this.warnDeprecated = warn; + return this; + } + + /** + * Enable translating old rule references to newer ones, if they have + * been moved or renamed. This is enabled by default, if disabled, + * unresolved references will not be translated and will produce an + * error. + * + * @return This instance, modified + */ + public RuleSetLoader enableCompatibility(boolean enable) { + this.enableCompatibility = enable; + return this; + } + + /** + * Follow deprecated rule references. By default this is off, + * and those references will be ignored (with a warning depending + * on {@link #enableCompatibility(boolean)}). + * + * @return This instance, modified + */ + public RuleSetLoader includeDeprecatedRuleReferences(boolean enable) { + this.includeDeprecatedRuleReferences = enable; + return this; + } + + /** + * Create a new rule set factory, if you have to (that class is deprecated). + * That factory will use the configuration that was set using the setters of this. + * + * @deprecated {@link RuleSetFactory} is deprecated, replace its usages + * with usages of this class, or of static factory methods of {@link RuleSet} + */ + @Deprecated + public RuleSetFactory toFactory() { + return new RuleSetFactory( + this.resourceLoader, + this.minimumPriority, + this.warnDeprecated, + this.enableCompatibility, + this.includeDeprecatedRuleReferences + ); + } + + + /** + * 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) + */ + public RuleSet loadFromResource(String rulesetPath) { + return loadFromResource(new RuleSetReferenceId(rulesetPath)); + } + + /** + * Parses several resources into a list of rulesets. + * + * @param paths Paths + * + * @throws RuleSetLoadException If any error occurs (eg, invalid syntax, or resource not found), + * for any of the parameters + * @throws NullPointerException If the parameter, or any component is null + */ + public List loadFromResources(Collection paths) { + List ruleSets = new ArrayList<>(paths.size()); + for (String path : paths) { + ruleSets.add(loadFromResource(path)); + } + return ruleSets; + } + + /** + * Parses several resources into a list of rulesets. + * + * @param first First path + * @param rest Paths + * + * @throws RuleSetLoadException If any error occurs (eg, invalid syntax, or resource not found), + * for any of the parameters + * @throws NullPointerException If the parameter, or any component is null + */ + public List loadFromResources(String first, String... rest) { + return loadFromResources(CollectionUtil.listOf(first, rest)); + } + + // package private + RuleSet loadFromResource(RuleSetReferenceId ruleSetReferenceId) { + try { + return toFactory().createRuleSet(ruleSetReferenceId); + } catch (Exception e) { + throw new RuleSetLoadException("Cannot parse " + ruleSetReferenceId, e); + } + } + + + /** + * Configure a new ruleset factory builder according to the parameters + * of the given PMD configuration. + */ + public static RuleSetLoader fromPmdConfig(PMDConfiguration configuration) { + return new RuleSetLoader().filterAbovePriority(configuration.getMinimumPriority()) + .enableCompatibility(configuration.isRuleSetFactoryCompatibilityEnabled()); + } + + + /** + * Returns an Iterator of RuleSet objects loaded from descriptions from the + * "categories.properties" resource for each language. This + * uses the classpath of the resource loader ({@link #loadResourcesWith(ClassLoader)}). + * + * @return A list of all category rulesets + * + * @throws RuleSetLoadException If a standard ruleset cannot be loaded. + * This is a corner case, that probably should not be caught by clients. + * The standard rulesets are well-formed, at least in stock PMD distributions. + * + */ + public List getStandardRuleSets() { + String rulesetsProperties; + List ruleSetReferenceIds = new ArrayList<>(); + for (Language language : LanguageRegistry.findWithRuleSupport()) { + Properties props = new Properties(); + rulesetsProperties = "category/" + language.getTerseName() + "/categories.properties"; + try (InputStream inputStream = resourceLoader.loadClassPathResourceAsStreamOrThrow(rulesetsProperties)) { + props.load(inputStream); + String rulesetFilenames = props.getProperty("rulesets.filenames"); + 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()); + } + } + } + + List ruleSets = new ArrayList<>(); + for (RuleSetReferenceId 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 index 0ad22f4c68..4e3acad5d4 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetNotFoundException.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetNotFoundException.java @@ -4,10 +4,20 @@ 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/RuleSetReference.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetReference.java index e7098568e1..cdc6e5f77c 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetReference.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetReference.java @@ -8,9 +8,15 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; +import net.sourceforge.pmd.annotation.InternalApi; + /** * This class represents a reference to RuleSet. + * + * @deprecated This is part of the internals of the {@link RuleSetLoader}. */ +@Deprecated +@InternalApi public class RuleSetReference { private final String ruleSetFileName; private final boolean allRules; 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 e5ee6c47cc..66dd5e47c9 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetReferenceId.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSetReferenceId.java @@ -14,6 +14,7 @@ import java.util.List; import org.apache.commons.lang3.StringUtils; +import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.util.ResourceLoader; /** @@ -78,7 +79,11 @@ import net.sourceforge.pmd.util.ResourceLoader; * * * + * + * @deprecated This is part of the internals of the {@link RuleSetLoader}. */ +@Deprecated +@InternalApi public class RuleSetReferenceId { private final boolean external; private final String ruleSetFileName; diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSets.java b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSets.java index bbeaba74fe..c107765f0a 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RuleSets.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RuleSets.java @@ -56,21 +56,31 @@ public class RuleSets { /** * Public constructor. Add the given rule set. * - * @param ruleSet - * the RuleSet + * @param ruleSet the RuleSet */ public RuleSets(RuleSet ruleSet) { addRuleSet(ruleSet); } + /** + * Aggregate the given rulesets. + */ + public RuleSets(List ruleSet) { + for (RuleSet set : ruleSet) { + addRuleSet(set); + } + } + /** * Add a ruleset for a language. Only one ruleset can be added for a * specific language. If ruleSet.getLanguage() is null, it is assumed to be * a RuleSet of java rules. * - * @param ruleSet - * the RuleSet + * @param ruleSet the RuleSet + * + * @deprecated Use {@link #RuleSets(List)} and don't mutate RuleSets after creation */ + @Deprecated public void addRuleSet(RuleSet ruleSet) { ruleSets.add(ruleSet); ruleChain.add(ruleSet); @@ -85,6 +95,11 @@ public class RuleSets { return ruleSets.toArray(new RuleSet[0]); } + // internal + List getRuleSetsInternal() { + return ruleSets; + } + public Iterator getRuleSetsIterator() { return ruleSets.iterator(); } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/RulesetsFactoryUtils.java b/pmd-core/src/main/java/net/sourceforge/pmd/RulesetsFactoryUtils.java index 69a1ff066c..135736f8a6 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/RulesetsFactoryUtils.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/RulesetsFactoryUtils.java @@ -13,6 +13,10 @@ 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()); @@ -76,7 +80,7 @@ public final class RulesetsFactoryUtils { } /** - * @deprecated Use {@link #createFactory(PMDConfiguration)} or {@link #createFactory(PMDConfiguration, ClassLoader)} + * @deprecated Use a {@link RuleSetLoader} */ @InternalApi @Deprecated @@ -96,7 +100,10 @@ public final class RulesetsFactoryUtils { * @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()); } @@ -107,7 +114,7 @@ public final class RulesetsFactoryUtils { * * @return A ruleset factory * - * @see #createFactory(PMDConfiguration, ClassLoader) + * @see RuleSetLoader */ public static RuleSetFactory defaultFactory() { return new RuleSetFactory(); @@ -124,7 +131,10 @@ public final class RulesetsFactoryUtils { * @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(), @@ -145,7 +155,10 @@ public final class RulesetsFactoryUtils { * @return A ruleset factory * * @see #createFactory(PMDConfiguration) + * + * @deprecated Use a {@link RuleSetLoader} */ + @Deprecated public static RuleSetFactory createFactory(ClassLoader classLoader, RulePriority minimumPriority, boolean warnDeprecated, @@ -166,11 +179,13 @@ public final class RulesetsFactoryUtils { * @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); } @@ -190,14 +205,16 @@ public final class RulesetsFactoryUtils { * @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); + includeDeprecatedRuleReferences); } /** diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/SourceCodeProcessor.java b/pmd-core/src/main/java/net/sourceforge/pmd/SourceCodeProcessor.java index 4722c89ec0..235e8faa9d 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/SourceCodeProcessor.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/SourceCodeProcessor.java @@ -11,6 +11,7 @@ import java.io.Reader; import java.util.Collections; import java.util.List; +import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.benchmark.TimeTracker; import net.sourceforge.pmd.benchmark.TimedOperation; import net.sourceforge.pmd.benchmark.TimedOperationCategory; @@ -23,6 +24,11 @@ import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.ast.ParseException; import net.sourceforge.pmd.lang.xpath.Initializer; +/** + * Source code processor is internal. + */ +@Deprecated +@InternalApi public class SourceCodeProcessor { private final PMDConfiguration configuration; 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 f78d78b8b9..6fe78b9856 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 @@ -9,11 +9,12 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.StringWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ContextedRuntimeException; import org.apache.tools.ant.AntClassLoader; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.DirectoryScanner; @@ -25,11 +26,9 @@ import net.sourceforge.pmd.PMD; import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.Report; import net.sourceforge.pmd.Rule; -import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.RulePriority; import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RuleSetFactory; -import net.sourceforge.pmd.RuleSetNotFoundException; +import net.sourceforge.pmd.RuleSetLoader; import net.sourceforge.pmd.RuleSets; import net.sourceforge.pmd.RulesetsFactoryUtils; import net.sourceforge.pmd.ant.Formatter; @@ -41,7 +40,6 @@ import net.sourceforge.pmd.renderers.AbstractRenderer; import net.sourceforge.pmd.renderers.Renderer; import net.sourceforge.pmd.util.ClasspathClassLoader; import net.sourceforge.pmd.util.IOUtil; -import net.sourceforge.pmd.util.ResourceLoader; import net.sourceforge.pmd.util.datasource.DataSource; import net.sourceforge.pmd.util.datasource.FileDataSource; import net.sourceforge.pmd.util.log.AntLogHandler; @@ -103,22 +101,20 @@ public class PMDTaskImpl { setupClassLoader(); // Setup RuleSetFactory and validate RuleSets - final ResourceLoader rl = setupResourceLoader(); - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.getRulesetFactory(configuration, rl); + RuleSetLoader rulesetLoader = RuleSetLoader.fromPmdConfig(configuration) + .loadResourcesWith(setupResourceLoader()); - try { - // This is just used to validate and display rules. Each thread will create its own ruleset - String ruleSets = configuration.getRuleSets(); - if (StringUtils.isNotBlank(ruleSets)) { - // Substitute env variables/properties - configuration.setRuleSets(project.replaceProperties(ruleSets)); - } - RuleSets rules = ruleSetFactory.createRuleSets(configuration.getRuleSets()); - logRulesUsed(rules); - } catch (RuleSetNotFoundException e) { - throw new BuildException(e.getMessage(), e); + // 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)); } + final RuleSets ruleSets = RulesetsFactoryUtils.getRuleSets(configuration.getRuleSets(), rulesetLoader.toFactory()); + List rulesetList = Arrays.asList(ruleSets.getAllRuleSets()); + logRulesUsed(ruleSets); + if (configuration.getSuppressMarker() != null) { project.log("Setting suppress marker to be " + configuration.getSuppressMarker(), Project.MSG_VERBOSE); } @@ -134,9 +130,8 @@ public class PMDTaskImpl { // TODO Do we really need all this in a loop over each FileSet? Seems // like a lot of redundancy - RuleContext ctx = new RuleContext(); Report errorReport = new Report(); - final AtomicInteger reportSize = new AtomicInteger(); + int problemCount = 0; final String separator = System.getProperty("file.separator"); for (FileSet fs : filesets) { @@ -169,10 +164,7 @@ public class PMDTaskImpl { @Override public void renderFileReport(Report r) { - int size = r.size(); - if (size > 0) { - reportSize.addAndGet(size); - } + // Nothing to do } @Override @@ -193,13 +185,19 @@ public class PMDTaskImpl { renderers.add(renderer); } try { - PMD.processFiles(configuration, ruleSetFactory, files, ctx, renderers); + Report report = PMD.processFiles(configuration, rulesetList, files, renderers); + problemCount += report.getViolations().size(); + } catch (ContextedRuntimeException e) { + if (e.getFirstContextValue("filename") instanceof String) { + handleError((String) e.getFirstContextValue("filename"), errorReport, e); + } else { + handleError("(unknown file)", errorReport, e); + } } catch (RuntimeException pmde) { - handleError(ctx, errorReport, pmde); + handleError("(unknown file)", errorReport, pmde); } } - int problemCount = reportSize.get(); project.log(problemCount + " problems found", Project.MSG_VERBOSE); for (Formatter formatter : formatters) { @@ -216,7 +214,7 @@ public class PMDTaskImpl { } } - private ResourceLoader setupResourceLoader() { + private ClassLoader setupResourceLoader() { if (classpath == null) { classpath = new Path(project); } @@ -233,11 +231,11 @@ public class PMDTaskImpl { // are loaded twice // and exist in multiple class loaders final boolean parentFirst = true; - return new ResourceLoader(new AntClassLoader(Thread.currentThread().getContextClassLoader(), - project, classpath, parentFirst)); + return new AntClassLoader(Thread.currentThread().getContextClassLoader(), + project, classpath, parentFirst); } - private void handleError(RuleContext ctx, Report errorReport, RuntimeException pmde) { + private void handleError(String filename, Report errorReport, RuntimeException pmde) { pmde.printStackTrace(); project.log(pmde.toString(), Project.MSG_VERBOSE); @@ -260,7 +258,7 @@ public class PMDTaskImpl { if (failOnError) { throw new BuildException(pmde); } - errorReport.addError(new Report.ProcessingError(pmde, String.valueOf(ctx.getSourceCodeFile()))); + errorReport.addError(new Report.ProcessingError(pmde, filename)); } private void setupClassLoader() { @@ -279,6 +277,10 @@ public class PMDTaskImpl { final ScopedLogHandlersManager logManager = new ScopedLogHandlersManager(antLogHandler.getAntLogLevel(), antLogHandler); try { doTask(); + } catch (BuildException e) { + throw e; + } catch (Exception other) { + throw new BuildException(other); } finally { logManager.close(); // only close the classloader, if it is ours. Otherwise we end up with class not found diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/benchmark/Benchmarker.java b/pmd-core/src/main/java/net/sourceforge/pmd/benchmark/Benchmarker.java index 42baa667b6..af56e1d1e0 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/benchmark/Benchmarker.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/benchmark/Benchmarker.java @@ -175,13 +175,12 @@ public final class Benchmarker { private static void stress(LanguageVersion languageVersion, RuleSet ruleSet, List dataSources, Set results, boolean debug) throws PMDException, IOException { - final RuleSetFactory factory = RulesetsFactoryUtils.defaultFactory(); for (Rule rule: ruleSet.getRules()) { if (debug) { System.out.println("Starting " + rule.getName()); } - final RuleSet working = factory.createSingleRuleRuleSet(rule); + final RuleSet working = RuleSet.forSingleRule(rule); RuleSets ruleSets = new RuleSets(working); PMDConfiguration config = new PMDConfiguration(); diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDCommandLineInterface.java b/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDCommandLineInterface.java index 04daf8b5dc..a8ad35ed4a 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDCommandLineInterface.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/cli/PMDCommandLineInterface.java @@ -8,6 +8,7 @@ import java.util.Properties; import net.sourceforge.pmd.PMD; import net.sourceforge.pmd.PMDVersion; +import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.properties.PropertyDescriptor; import net.sourceforge.pmd.renderers.Renderer; @@ -19,7 +20,10 @@ import com.beust.jcommander.ParameterException; /** * @author Romain Pelisse <belaran@gmail.com> * + * @deprecated Internal API. Use {@link PMD#run(String[])} or {@link PMD#main(String[])} */ +@Deprecated +@InternalApi public final class PMDCommandLineInterface { public static final String PROG_NAME = "pmd"; 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 b68cb336a3..94cd430553 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 @@ -9,8 +9,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Properties; +import net.sourceforge.pmd.PMD; import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.RulePriority; +import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.LanguageVersion; @@ -20,6 +22,11 @@ import com.beust.jcommander.Parameter; import com.beust.jcommander.ParameterException; import com.beust.jcommander.validators.PositiveInteger; +/** + * @deprecated Internal API. Use {@link PMD#run(String[])} or {@link PMD#main(String[])} + */ +@Deprecated +@InternalApi public class PMDParameters { @Parameter(names = { "-rulesets", "-R" }, description = "Comma separated list of ruleset names to use.", 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 1a5d5b20fc..b4702c32db 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,6 +4,12 @@ package net.sourceforge.pmd.lang; +import java.util.Locale; +import java.util.Objects; + +import net.sourceforge.pmd.properties.AbstractPropertySource; +import net.sourceforge.pmd.properties.PropertyDescriptor; + /** * Represents a set of configuration options for a {@link Parser}. For each * unique combination of ParserOptions a Parser will be used to create an AST. @@ -17,6 +23,23 @@ public class ParserOptions { @Deprecated protected String suppressMarker; + /** + * 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 String getSuppressMarker() { return suppressMarker; } @@ -25,6 +48,23 @@ public class ParserOptions { this.suppressMarker = suppressMarker; } + protected final void defineProperty(PropertyDescriptor propertyDescriptor) { + parserOptionsProperties.definePropertyDescriptor(propertyDescriptor); + } + + 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 public boolean equals(Object obj) { if (this == obj) { @@ -34,11 +74,84 @@ public class ParserOptions { return false; } final ParserOptions that = (ParserOptions) obj; - return this.suppressMarker.equals(that.suppressMarker); + return Objects.equals(suppressMarker, that.suppressMarker) + && Objects.equals(languageId, that.languageId) + && Objects.equals(parserOptionsProperties, that.parserOptionsProperties); } @Override public int hashCode() { - return suppressMarker != null ? suppressMarker.hashCode() : 0; + 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 07552dd3e8..5bcf538f21 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 @@ -12,6 +12,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.exception.ContextedRuntimeException; import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.Report; @@ -21,6 +22,7 @@ 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; import net.sourceforge.pmd.benchmark.TimedOperation; import net.sourceforge.pmd.benchmark.TimedOperationCategory; @@ -30,7 +32,10 @@ import net.sourceforge.pmd.util.datasource.DataSource; /** * @author Romain Pelisse <belaran@gmail.com> * + * @deprecated Is internal API */ +@Deprecated +@InternalApi public abstract class AbstractPMDProcessor { private static final Logger LOG = Logger.getLogger(AbstractPMDProcessor.class.getName()); @@ -75,13 +80,15 @@ public abstract class AbstractPMDProcessor { */ 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) { report.addConfigError(new Report.ConfigurationError(rule, rule.dysfunctionReason())); } - - return rs; } /** @@ -91,7 +98,7 @@ public abstract class AbstractPMDProcessor { * @param ruleSets RuleSets to prune of broken rules. * @return Set */ - private Set removeBrokenRules(final RuleSets ruleSets) { + private static Set removeBrokenRules(final RuleSets ruleSets) { final Set brokenRules = new HashSet<>(); ruleSets.removeDysfunctionalRules(brokenRules); @@ -105,28 +112,37 @@ public abstract class AbstractPMDProcessor { return brokenRules; } + @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 - public void processFiles(RuleSetFactory ruleSetFactory, List files, RuleContext ctx, - List renderers) { + public void processFiles(RuleSets rulesets, List files, RuleContext ctx, List renderers) { try { - final RuleSets rs = createRuleSets(ruleSetFactory, ctx.getReport()); - configuration.getAnalysisCache().checkValidity(rs, configuration.getClassLoader()); + reportBrokenRules(ctx.getReport(), rulesets); + + // render base report first - general errors + renderReports(renderers, ctx.getReport()); + + configuration.getAnalysisCache().checkValidity(rulesets, configuration.getClassLoader()); final SourceCodeProcessor processor = new SourceCodeProcessor(configuration); for (final DataSource dataSource : files) { // this is the real, canonical and absolute filename (not shortened) String realFileName = dataSource.getNiceFileName(false, null); - runAnalysis(new PmdRunnable(dataSource, realFileName, renderers, ctx, rs, processor)); + runAnalysis(new PmdRunnable(dataSource, realFileName, renderers, ctx, rulesets, processor)); } - // render base report first - general errors - renderReports(renderers, ctx.getReport()); - // then add analysis results per file collectReports(renderers); + } catch (RuntimeException e) { + throw new ContextedRuntimeException(e).addContextValue("filename", String.valueOf(ctx.getSourceCodeFile())); } finally { // in case we analyzed files within Zip Files/Jars, we need to close them after // the analysis is finished diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/processor/MonoThreadProcessor.java b/pmd-core/src/main/java/net/sourceforge/pmd/processor/MonoThreadProcessor.java index 6acf61a32c..f2776574e4 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/processor/MonoThreadProcessor.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/processor/MonoThreadProcessor.java @@ -9,12 +9,15 @@ import java.util.List; import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.Report; +import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.renderers.Renderer; /** * @author Romain Pelisse <belaran@gmail.com> - * + * @deprecated Is internal API */ +@Deprecated +@InternalApi public final class MonoThreadProcessor extends AbstractPMDProcessor { private final List reports = new ArrayList<>(); diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/processor/MultiThreadProcessor.java b/pmd-core/src/main/java/net/sourceforge/pmd/processor/MultiThreadProcessor.java index f398a90db8..8745f99bf0 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/processor/MultiThreadProcessor.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/processor/MultiThreadProcessor.java @@ -13,12 +13,16 @@ import java.util.concurrent.Executors; import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.Report; +import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.renderers.Renderer; /** * @author Romain Pelisse <belaran@gmail.com> + * @deprecated Is internal API */ +@Deprecated +@InternalApi public class MultiThreadProcessor extends AbstractPMDProcessor { private final ExecutorService executor; private final CompletionService completionService; diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java b/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java index d3ddeb6230..da91fbd95a 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdRunnable.java @@ -17,10 +17,17 @@ import net.sourceforge.pmd.Report; import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.RuleSets; import net.sourceforge.pmd.SourceCodeProcessor; +import net.sourceforge.pmd.annotation.InternalApi; import net.sourceforge.pmd.benchmark.TimeTracker; import net.sourceforge.pmd.renderers.Renderer; import net.sourceforge.pmd.util.datasource.DataSource; +/** + * + * @deprecated Is internal API + */ +@Deprecated +@InternalApi public class PmdRunnable implements Callable { private static final Logger LOG = Logger.getLogger(PmdRunnable.class.getName()); @@ -86,6 +93,9 @@ public class PmdRunnable implements Callable { TimeTracker.finishThread(); + // merge the sub-report into the global report (thread-safe) + ruleContext.getReport().merge(report); + return report; } diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdThreadFactory.java b/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdThreadFactory.java index 51c84b3063..e767b8e79b 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdThreadFactory.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/processor/PmdThreadFactory.java @@ -7,6 +7,14 @@ package net.sourceforge.pmd.processor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; +import net.sourceforge.pmd.annotation.InternalApi; + +/** + * + * @deprecated Is internal API + */ +@Deprecated +@InternalApi public class PmdThreadFactory implements ThreadFactory { private final AtomicInteger counter = new AtomicInteger(); 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 71463940cf..855a232e22 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 @@ -252,4 +252,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/renderers/CodeClimateRenderer.java b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/CodeClimateRenderer.java index c738fac851..7430d4bb6f 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/renderers/CodeClimateRenderer.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/renderers/CodeClimateRenderer.java @@ -84,12 +84,16 @@ public class CodeClimateRenderer extends AbstractIncrementingRenderer { switch (rule.getPriority()) { case HIGH: - issue.severity = "critical"; + issue.severity = "blocker"; break; case MEDIUM_HIGH: + issue.severity = "critical"; + break; case MEDIUM: + issue.severity = "major"; + break; case MEDIUM_LOW: - issue.severity = "normal"; + issue.severity = "minor"; break; case LOW: default: diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/util/designer/Designer.java b/pmd-core/src/main/java/net/sourceforge/pmd/util/designer/Designer.java index 1ece0350ac..588ff6f7ff 100644 --- a/pmd-core/src/main/java/net/sourceforge/pmd/util/designer/Designer.java +++ b/pmd-core/src/main/java/net/sourceforge/pmd/util/designer/Designer.java @@ -103,7 +103,6 @@ import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.PMDVersion; import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RuleSetFactory; import net.sourceforge.pmd.RuleSets; import net.sourceforge.pmd.SourceCodeProcessor; import net.sourceforge.pmd.lang.LanguageRegistry; @@ -564,7 +563,7 @@ public class Designer implements ClipboardOwner { LanguageVersion languageVersion = getLanguageVersion(); DFAGraphRule dfaGraphRule = languageVersion.getLanguageVersionHandler().getDFAGraphRule(); if (dfaGraphRule != null) { - final RuleSet rs = new RuleSetFactory().createSingleRuleRuleSet(dfaGraphRule); + final RuleSet rs = RuleSet.forSingleRule(dfaGraphRule); RuleContext ctx = new RuleContext(); ctx.setSourceCodeFile(new File("[no filename]." + languageVersion.getLanguage().getExtensions().get(0))); PMDConfiguration config = new PMDConfiguration(); @@ -610,7 +609,7 @@ public class Designer implements ClipboardOwner { xpathRule.setXPath(xpathQueryArea.getText()); xpathRule.setVersion(xpathVersionButtonGroup.getSelection().getActionCommand()); - final RuleSet ruleSet = new RuleSetFactory().createSingleRuleRuleSet(xpathRule); + final RuleSet ruleSet = RuleSet.forSingleRule(xpathRule); RuleSets ruleSets = new RuleSets(ruleSet); diff --git a/pmd-core/src/main/resources/rulesets/releases/6310.xml b/pmd-core/src/main/resources/rulesets/releases/6310.xml new file mode 100644 index 0000000000..eaf61d2469 --- /dev/null +++ b/pmd-core/src/main/resources/rulesets/releases/6310.xml @@ -0,0 +1,13 @@ + + + + +This ruleset contains links to rules that are new in PMD v6.31.0 + + + + + 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 1732c1f732..bfbc7ce547 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetFactoryTest.java @@ -505,40 +505,40 @@ public class RuleSetFactoryTest { @Test public void testReferencePriority() throws RuleSetNotFoundException { - ResourceLoader rl = new ResourceLoader(); - RuleSetFactory rsf = new RuleSetFactory(rl, RulePriority.LOW, false, true); + 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)); assertEquals("Number of Rules", 3, ruleSet.getRules().size()); assertNotNull(ruleSet.getRuleByName("MockRuleName")); assertNotNull(ruleSet.getRuleByName("MockRuleNameRef")); assertNotNull(ruleSet.getRuleByName("MockRuleNameRefRef")); - rsf = new RuleSetFactory(rl, RulePriority.MEDIUM_HIGH, false, true); + rsf = config.filterAbovePriority(RulePriority.MEDIUM_HIGH).toFactory(); ruleSet = rsf.createRuleSet(createRuleSetReferenceId(REF_INTERNAL_TO_INTERNAL_CHAIN)); assertEquals("Number of Rules", 2, ruleSet.getRules().size()); assertNotNull(ruleSet.getRuleByName("MockRuleNameRef")); assertNotNull(ruleSet.getRuleByName("MockRuleNameRefRef")); - rsf = new RuleSetFactory(rl, RulePriority.HIGH, false, true); + rsf = config.filterAbovePriority(RulePriority.HIGH).toFactory(); ruleSet = rsf.createRuleSet(createRuleSetReferenceId(REF_INTERNAL_TO_INTERNAL_CHAIN)); assertEquals("Number of Rules", 1, ruleSet.getRules().size()); assertNotNull(ruleSet.getRuleByName("MockRuleNameRefRef")); - rsf = new RuleSetFactory(rl, RulePriority.LOW, false, true); + rsf = config.filterAbovePriority(RulePriority.LOW).toFactory(); ruleSet = rsf.createRuleSet(createRuleSetReferenceId(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 = new RuleSetFactory(rl, RulePriority.MEDIUM_HIGH, false, true); + rsf = config.filterAbovePriority(RulePriority.MEDIUM_HIGH).toFactory(); ruleSet = rsf.createRuleSet(createRuleSetReferenceId(REF_INTERNAL_TO_EXTERNAL_CHAIN)); assertEquals("Number of Rules", 2, ruleSet.getRules().size()); assertNotNull(ruleSet.getRuleByName("ExternalRefRuleNameRef")); assertNotNull(ruleSet.getRuleByName("ExternalRefRuleNameRefRef")); - rsf = new RuleSetFactory(rl, RulePriority.HIGH, false, true); + rsf = config.filterAbovePriority(RulePriority.HIGH).toFactory(); ruleSet = rsf.createRuleSet(createRuleSetReferenceId(REF_INTERNAL_TO_EXTERNAL_CHAIN)); assertEquals("Number of Rules", 1, ruleSet.getRules().size()); assertNotNull(ruleSet.getRuleByName("ExternalRefRuleNameRefRef")); @@ -546,7 +546,7 @@ public class RuleSetFactoryTest { @Test public void testOverridePriorityLoadWithMinimum() throws RuleSetNotFoundException { - RuleSetFactory rsf = new RuleSetFactory(new ResourceLoader(), RulePriority.MEDIUM_LOW, true, true); + 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"); // only one rule should remain, since we filter out the other rule by minimum priority assertEquals("Number of Rules", 1, ruleset.getRules().size()); @@ -567,13 +567,13 @@ public class RuleSetFactoryTest { @Test public void testExcludeWithMinimumPriority() throws RuleSetNotFoundException { - RuleSetFactory rsf = RulesetsFactoryUtils.createFactory(RulePriority.HIGH, true, true); + RuleSetFactory rsf = new RuleSetLoader().filterAbovePriority(RulePriority.HIGH).toFactory(); RuleSet ruleset = rsf.createRuleSet("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 = RulesetsFactoryUtils.defaultFactory(); + rsf = new RuleSetLoader().filterAbovePriority(RulePriority.LOW).toFactory(); ruleset = rsf.createRuleSet("net/sourceforge/pmd/rulesets/ruleset-minimum-priority-exclusion.xml"); // only one rule, we have excluded one... assertEquals("Number of Rules", 1, ruleset.getRules().size()); @@ -602,10 +602,9 @@ public class RuleSetFactoryTest { @Test public void testSetPriority() throws RuleSetNotFoundException { - ResourceLoader rl = new ResourceLoader(); - RuleSetFactory rsf = new RuleSetFactory(rl, RulePriority.MEDIUM_HIGH, false, true); + RuleSetFactory rsf = new RuleSetLoader().filterAbovePriority(RulePriority.MEDIUM_HIGH).warnDeprecated(false).toFactory(); assertEquals(0, rsf.createRuleSet(createRuleSetReferenceId(SINGLE_RULE)).size()); - rsf = new RuleSetFactory(rl, RulePriority.MEDIUM_LOW, false, true); + rsf = new RuleSetLoader().filterAbovePriority(RulePriority.MEDIUM_LOW).warnDeprecated(false).toFactory(); assertEquals(1, rsf.createRuleSet(createRuleSetReferenceId(SINGLE_RULE)).size()); } @@ -780,7 +779,7 @@ public class RuleSetFactoryTest { + " Ruleset which references a empty ruleset\n" + "\n" + " \n" + "\n"); - RuleSetFactory ruleSetFactory = new RuleSetFactory(new ResourceLoader(), RulePriority.LOW, true, true); + RuleSetFactory ruleSetFactory = new RuleSetLoader().loadResourcesWith(new ResourceLoader()).filterAbovePriority(RulePriority.LOW).warnDeprecated(true).enableCompatibility(true).toFactory(); RuleSet ruleset = ruleSetFactory.createRuleSet(ref); assertEquals(0, ruleset.getRules().size()); @@ -1285,7 +1284,7 @@ public class RuleSetFactoryTest { } private RuleSet loadRuleSetWithDeprecationWarnings(String ruleSetXml) throws RuleSetNotFoundException { - RuleSetFactory rsf = RulesetsFactoryUtils.createFactory(RulePriority.LOW, true, false); + RuleSetFactory rsf = new RuleSetLoader().warnDeprecated(true).enableCompatibility(false).toFactory(); return rsf.createRuleSet(createRuleSetReferenceId(ruleSetXml)); } 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 7a275bc011..7c0f5c8813 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetTest.java @@ -60,7 +60,7 @@ public class RuleSetTest { public void testNoDFA() { MockRule mock = new MockRule("name", "desc", "msg", "rulesetname"); mock.setLanguage(LanguageRegistry.getLanguage(DummyLanguageModule.NAME)); - RuleSet rs = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(mock); + RuleSet rs = RuleSet.forSingleRule(mock); assertFalse(rs.usesDFA(LanguageRegistry.getLanguage(DummyLanguageModule.NAME))); } @@ -69,7 +69,7 @@ public class RuleSetTest { MockRule mock = new MockRule("name", "desc", "msg", "rulesetname"); mock.setLanguage(LanguageRegistry.getLanguage(DummyLanguageModule.NAME)); mock.setDfa(true); - RuleSet rs = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(mock); + RuleSet rs = RuleSet.forSingleRule(mock); assertTrue(rs.usesDFA(LanguageRegistry.getLanguage(DummyLanguageModule.NAME))); } @@ -88,21 +88,21 @@ public class RuleSetTest { @Test public void testGetRuleByName() { MockRule mock = new MockRule("name", "desc", "msg", "rulesetname"); - RuleSet rs = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(mock); + RuleSet rs = RuleSet.forSingleRule(mock); assertEquals("unable to fetch rule by name", mock, rs.getRuleByName("name")); } @Test public void testGetRuleByName2() { MockRule mock = new MockRule("name", "desc", "msg", "rulesetname"); - RuleSet rs = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(mock); + RuleSet rs = RuleSet.forSingleRule(mock); assertNull("the rule FooRule must not be found!", rs.getRuleByName("FooRule")); } @Test public void testRuleList() { MockRule rule = new MockRule("name", "desc", "msg", "rulesetname"); - RuleSet ruleset = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(rule); + RuleSet ruleset = RuleSet.forSingleRule(rule); assertEquals("Size of RuleSet isn't one.", 1, ruleset.size()); 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 d488f30fcc..6f6e1643dc 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetWriterTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/RuleSetWriterTest.java @@ -81,7 +81,7 @@ public class RuleSetWriterTest { ruleRef.setRuleSetReference(ruleSetReference); ruleRef.setName("Foo"); // override the name - RuleSet ruleSet = ruleSetFactory.createSingleRuleRuleSet(ruleRef); + RuleSet ruleSet = RuleSet.forSingleRule(ruleRef); writer.write(ruleSet); diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/ant/PMDTaskTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/ant/PMDTaskTest.java index 0bdf85b5ad..cad90eac45 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/ant/PMDTaskTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/ant/PMDTaskTest.java @@ -7,7 +7,6 @@ package net.sourceforge.pmd.ant; import static org.junit.Assert.fail; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -71,7 +70,7 @@ public class PMDTaskTest { } @Test - public void testWithShortFilenames() throws FileNotFoundException, IOException { + public void testWithShortFilenames() throws IOException { buildRule.executeTarget("testWithShortFilenames"); try (InputStream in = new FileInputStream("target/pmd-ant-test.txt")) { diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/properties/PropertyDescriptorTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/properties/PropertyDescriptorTest.java index bba31ae3dc..7c82a7fa3e 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/properties/PropertyDescriptorTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/properties/PropertyDescriptorTest.java @@ -30,7 +30,6 @@ import org.junit.rules.ExpectedException; import net.sourceforge.pmd.FooRule; import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RulesetsFactoryUtils; import net.sourceforge.pmd.properties.constraints.PropertyConstraint; @@ -57,7 +56,7 @@ public class PropertyDescriptorTest { FooRule rule = new FooRule(); rule.definePropertyDescriptor(intProperty); rule.setProperty(intProperty, 1000); - RuleSet ruleSet = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(rule); + RuleSet ruleSet = RuleSet.forSingleRule(rule); List dysfunctional = new ArrayList<>(); ruleSet.removeDysfunctionalRules(dysfunctional); @@ -78,7 +77,7 @@ public class PropertyDescriptorTest { FooRule rule = new FooRule(); rule.definePropertyDescriptor(descriptor); rule.setProperty(descriptor, Collections.singletonList(1000d)); // not in range - RuleSet ruleSet = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(rule); + RuleSet ruleSet = RuleSet.forSingleRule(rule); List dysfunctional = new ArrayList<>(); ruleSet.removeDysfunctionalRules(dysfunctional); diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/renderers/AbstractRendererTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/renderers/AbstractRendererTest.java index 97e0e6664d..bff13f27ef 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/renderers/AbstractRendererTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/renderers/AbstractRendererTest.java @@ -16,6 +16,7 @@ import net.sourceforge.pmd.Report.ConfigurationError; import net.sourceforge.pmd.Report.ProcessingError; import net.sourceforge.pmd.ReportTest; import net.sourceforge.pmd.RuleContext; +import net.sourceforge.pmd.RulePriority; import net.sourceforge.pmd.RuleViolation; import net.sourceforge.pmd.RuleWithProperties; import net.sourceforge.pmd.lang.ast.DummyNode; @@ -69,8 +70,12 @@ public abstract class AbstractRendererTest { private Report reportTwoViolations() { Report report = new Report(); - report.addRuleViolation(newRuleViolation(1)); - report.addRuleViolation(newRuleViolation(2)); + RuleViolation informationalRuleViolation = newRuleViolation(1); + informationalRuleViolation.getRule().setPriority(RulePriority.LOW); + report.addRuleViolation(informationalRuleViolation); + RuleViolation severeRuleViolation = newRuleViolation(2); + severeRuleViolation.getRule().setPriority(RulePriority.HIGH); + report.addRuleViolation(severeRuleViolation); return report; } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/renderers/CSVRendererTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/renderers/CSVRendererTest.java index 029dd303d8..5ef24218fe 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/renderers/CSVRendererTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/renderers/CSVRendererTest.java @@ -30,7 +30,7 @@ public class CSVRendererTest extends AbstractRendererTest { public String getExpectedMultiple() { return getHeader() + "\"1\",\"\",\"" + getSourceCodeFilename() + "\",\"5\",\"1\",\"blah\",\"RuleSet\",\"Foo\"" + PMD.EOL - + "\"2\",\"\",\"" + getSourceCodeFilename() + "\",\"5\",\"1\",\"blah\",\"RuleSet\",\"Foo\"" + PMD.EOL; + + "\"2\",\"\",\"" + getSourceCodeFilename() + "\",\"1\",\"1\",\"blah\",\"RuleSet\",\"Foo\"" + PMD.EOL; } @Override diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/renderers/CodeClimateRendererTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/renderers/CodeClimateRendererTest.java index 089c8d12a2..ca24c5a909 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/renderers/CodeClimateRendererTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/renderers/CodeClimateRendererTest.java @@ -76,7 +76,7 @@ public class CodeClimateRendererTest extends AbstractRendererTest { + "violationSuppressXPath | | Suppress violations on nodes which match a given relative XPath expression.\\n" + "\"},\"categories\":[\"Style\"],\"location\":{\"path\":\"" + getSourceCodeFilename() + "\",\"lines\":{\"begin\":1,\"end\":1}},\"severity\":\"info\",\"remediation_points\":50000}" + "\u0000" + PMD.EOL + "{\"type\":\"issue\",\"check_name\":\"Foo\",\"description\":\"blah\"," - + "\"content\":{\"body\":\"## Foo\\n\\nSince: PMD null\\n\\nPriority: Low\\n\\n" + + "\"content\":{\"body\":\"## Foo\\n\\nSince: PMD null\\n\\nPriority: High\\n\\n" + "[Categories](https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#categories): Style\\n\\n" + "[Remediation Points](https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#remediation-points): 50000\\n\\n" + "desc\\n\\n" @@ -84,7 +84,7 @@ public class CodeClimateRendererTest extends AbstractRendererTest { + "Name | Value | Description\\n" + "--- | --- | ---\\n" + "violationSuppressRegex | | Suppress violations with messages matching a regular expression\\n" + "violationSuppressXPath | | Suppress violations on nodes which match a given relative XPath expression.\\n" - + "\"},\"categories\":[\"Style\"],\"location\":{\"path\":\"" + getSourceCodeFilename() + "\",\"lines\":{\"begin\":1,\"end\":1}},\"severity\":\"info\",\"remediation_points\":50000}" + + "\"},\"categories\":[\"Style\"],\"location\":{\"path\":\"" + getSourceCodeFilename() + "\",\"lines\":{\"begin\":1,\"end\":1}},\"severity\":\"blocker\",\"remediation_points\":50000}" + "\u0000" + PMD.EOL; } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/renderers/JsonRendererTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/renderers/JsonRendererTest.java index 2a1f08fd3c..524277fe21 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/renderers/JsonRendererTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/renderers/JsonRendererTest.java @@ -79,6 +79,7 @@ public class JsonRendererTest extends AbstractRendererTest { public String filter(String expected) { String result = expected .replaceAll("\"timestamp\":\\s*\"[^\"]+\"", "\"timestamp\": \"--replaced--\"") + .replaceAll("\"pmdVersion\":\\s*\"[^\"]+\"", "\"pmdVersion\": \"unknown\"") .replaceAll("\r\n", "\n"); // make the test run on Windows, too return result; } diff --git a/pmd-core/src/test/java/net/sourceforge/pmd/renderers/XMLRendererTest.java b/pmd-core/src/test/java/net/sourceforge/pmd/renderers/XMLRendererTest.java index 62e628c106..6e11717f08 100644 --- a/pmd-core/src/test/java/net/sourceforge/pmd/renderers/XMLRendererTest.java +++ b/pmd-core/src/test/java/net/sourceforge/pmd/renderers/XMLRendererTest.java @@ -63,7 +63,7 @@ public class XMLRendererTest extends AbstractRendererTest { return getHeader() + "" + PMD.EOL + "" + PMD.EOL + "blah" + PMD.EOL + "" + PMD.EOL - + "" + + "" + PMD.EOL + "blah" + PMD.EOL + "" + PMD.EOL + "" + PMD.EOL + "" + PMD.EOL; } diff --git a/pmd-core/src/test/resources/net/sourceforge/pmd/renderers/json/expected-multiple.json b/pmd-core/src/test/resources/net/sourceforge/pmd/renderers/json/expected-multiple.json index 15717aed32..9ab3704f1e 100644 --- a/pmd-core/src/test/resources/net/sourceforge/pmd/renderers/json/expected-multiple.json +++ b/pmd-core/src/test/resources/net/sourceforge/pmd/renderers/json/expected-multiple.json @@ -24,7 +24,7 @@ "description": "blah", "rule": "Foo", "ruleset": "RuleSet", - "priority": 5 + "priority": 1 } ] } diff --git a/pmd-cpp/pom.xml b/pmd-cpp/pom.xml index 1946fd4b5a..217ee661f8 100644 --- a/pmd-cpp/pom.xml +++ b/pmd-cpp/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-cpp/src/main/ant/alljavacc.xml b/pmd-cpp/src/main/ant/alljavacc.xml index aa41ee0817..dd0e00a7e2 100644 --- a/pmd-cpp/src/main/ant/alljavacc.xml +++ b/pmd-cpp/src/main/ant/alljavacc.xml @@ -29,11 +29,15 @@ - + + + + + + + diff --git a/pmd-cs/pom.xml b/pmd-cs/pom.xml index 86ea909c53..e850c8e190 100644 --- a/pmd-cs/pom.xml +++ b/pmd-cs/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-cs/src/main/antlr4/net/sourceforge/pmd/lang/cs/antlr4/CSharpLexer.g4 b/pmd-cs/src/main/antlr4/net/sourceforge/pmd/lang/cs/antlr4/CSharpLexer.g4 index f381392fa9..e1c213b93b 100644 --- a/pmd-cs/src/main/antlr4/net/sourceforge/pmd/lang/cs/antlr4/CSharpLexer.g4 +++ b/pmd-cs/src/main/antlr4/net/sourceforge/pmd/lang/cs/antlr4/CSharpLexer.g4 @@ -19,7 +19,8 @@ private boolean verbatium; BYTE_ORDER_MARK: '\u00EF\u00BB\u00BF'; SINGLE_LINE_DOC_COMMENT: '///' InputCharacter* -> channel(COMMENTS_CHANNEL); -DELIMITED_DOC_COMMENT: '/**' .*? '*/' -> channel(COMMENTS_CHANNEL); +EMPTY_DELIMITED_DOC_COMMENT: '/***/' -> channel(COMMENTS_CHANNEL); +DELIMITED_DOC_COMMENT: '/**' ~'/' .*? '*/' -> channel(COMMENTS_CHANNEL); SINGLE_LINE_COMMENT: '//' InputCharacter* -> channel(COMMENTS_CHANNEL); DELIMITED_COMMENT: '/*' .*? '*/' -> channel(COMMENTS_CHANNEL); @@ -119,6 +120,7 @@ TYPEOF: 'typeof'; UINT: 'uint'; ULONG: 'ulong'; UNCHECKED: 'unchecked'; +UNMANAGED: 'unmanaged'; UNSAFE: 'unsafe'; USHORT: 'ushort'; USING: 'using'; @@ -138,10 +140,11 @@ IDENTIFIER: '@'? IdentifierOrKeyword; //B.1.8 Literals // 0.Equals() would be parsed as an invalid real (1. branch) causing a lexer error -LITERAL_ACCESS: [0-9]+ IntegerTypeSuffix? '.' '@'? IdentifierOrKeyword; -INTEGER_LITERAL: [0-9]+ IntegerTypeSuffix?; -HEX_INTEGER_LITERAL: '0' [xX] HexDigit+ IntegerTypeSuffix?; -REAL_LITERAL: [0-9]* '.' [0-9]+ ExponentPart? [FfDdMm]? | [0-9]+ ([FfDdMm] | ExponentPart [FfDdMm]?); +LITERAL_ACCESS: [0-9] ('_'* [0-9])* IntegerTypeSuffix? '.' '@'? IdentifierOrKeyword; +INTEGER_LITERAL: [0-9] ('_'* [0-9])* IntegerTypeSuffix?; +HEX_INTEGER_LITERAL: '0' [xX] ('_'* HexDigit)+ IntegerTypeSuffix?; +BIN_INTEGER_LITERAL: '0' [bB] ('_'* [01])+ IntegerTypeSuffix?; +REAL_LITERAL: ([0-9] ('_'* [0-9])*)? '.' [0-9] ('_'* [0-9])* ExponentPart? [FfDdMm]? | [0-9] ('_'* [0-9])* ([FfDdMm] | ExponentPart [FfDdMm]?); CHARACTER_LITERAL: '\'' (~['\\\r\n\u0085\u2028\u2029] | CommonCharacter) '\''; REGULAR_STRING: '"' (~["\\\r\n\u0085\u2028\u2029] | CommonCharacter)* '"'; @@ -234,6 +237,8 @@ OP_OR_ASSIGNMENT: '|='; OP_XOR_ASSIGNMENT: '^='; OP_LEFT_SHIFT: '<<'; OP_LEFT_SHIFT_ASSIGNMENT: '<<='; +OP_COALESCING_ASSIGNMENT: '??='; +OP_RANGE: '..'; // https://msdn.microsoft.com/en-us/library/dn961160.aspx mode INTERPOLATION_STRING; @@ -271,6 +276,7 @@ WARNING: 'warning' Whitespace+ -> channel(DIREC REGION: 'region' Whitespace* -> channel(DIRECTIVE), mode(DIRECTIVE_TEXT); ENDREGION: 'endregion' Whitespace* -> channel(DIRECTIVE), mode(DIRECTIVE_TEXT); PRAGMA: 'pragma' Whitespace+ -> channel(DIRECTIVE), mode(DIRECTIVE_TEXT); +NULLABLE: 'nullable' Whitespace+ -> channel(DIRECTIVE), mode(DIRECTIVE_TEXT); DIRECTIVE_DEFAULT: 'default' -> channel(DIRECTIVE), type(DEFAULT); DIRECTIVE_HIDDEN: 'hidden' -> channel(DIRECTIVE); DIRECTIVE_OPEN_PARENS: '(' -> channel(DIRECTIVE), type(OPEN_PARENS); @@ -303,7 +309,7 @@ fragment NewLineCharacter ; fragment IntegerTypeSuffix: [lL]? [uU] | [uU]? [lL]; -fragment ExponentPart: [eE] ('+' | '-')? [0-9]+; +fragment ExponentPart: [eE] ('+' | '-')? [0-9] ('_'* [0-9])*; fragment CommonCharacter : SimpleEscapeSequence @@ -1102,4 +1108,4 @@ fragment UnicodeClassND | '\uaa50'..'\uaa59' | '\uabf0'..'\uabf9' | '\uff10'..'\uff19' - ; \ No newline at end of file + ; diff --git a/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsTokenizer.java b/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsTokenizer.java index e70313314d..a48212290a 100644 --- a/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsTokenizer.java +++ b/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsTokenizer.java @@ -71,7 +71,7 @@ public class CsTokenizer extends AntlrTokenizer { private final boolean ignoreLiteralSequences; private boolean discardingUsings = false; private boolean discardingNL = false; - private boolean discardingLiterals = false; + private AntlrToken discardingLiteralsUntil = null; private boolean discardCurrent = false; CsTokenFilter(final AntlrTokenManager tokenManager, boolean ignoreUsings, boolean ignoreLiteralSequences) { @@ -169,19 +169,24 @@ public class CsTokenizer extends AntlrTokenizer { private void skipLiteralSequences(final AntlrToken currentToken, final Iterable remainingTokens) { if (ignoreLiteralSequences) { final int type = currentToken.getKind(); - if (type == CSharpLexer.OPEN_BRACE && isSequenceOfLiterals(remainingTokens)) { - discardingLiterals = true; - } else if (type == CSharpLexer.CLOSE_BRACE && discardingLiterals) { - discardingLiterals = false; - discardCurrent = true; + if (isDiscardingLiterals()) { + if (currentToken == discardingLiteralsUntil) { // NOPMD - intentional check for reference equality + discardingLiteralsUntil = null; + discardCurrent = true; + } + } else if (type == CSharpLexer.OPEN_BRACE) { + final AntlrToken finalToken = findEndOfSequenceOfLiterals(remainingTokens); + discardingLiteralsUntil = finalToken; } } } - private boolean isSequenceOfLiterals(final Iterable remainingTokens) { + private AntlrToken findEndOfSequenceOfLiterals(final Iterable remainingTokens) { boolean seenLiteral = false; + int braceCount = 0; for (final AntlrToken token : remainingTokens) { switch (token.getKind()) { + case CSharpLexer.BIN_INTEGER_LITERAL: case CSharpLexer.CHARACTER_LITERAL: case CSharpLexer.HEX_INTEGER_LITERAL: case CSharpLexer.INTEGER_LITERAL: @@ -190,20 +195,33 @@ public class CsTokenizer extends AntlrTokenizer { break; // can be skipped; continue to the next token case CSharpLexer.COMMA: break; // can be skipped; continue to the next token + case CSharpLexer.OPEN_BRACE: + braceCount++; + break; // curly braces are allowed, as long as they're balanced case CSharpLexer.CLOSE_BRACE: - // end of the list; skip all contents - return seenLiteral; + braceCount--; + if (braceCount < 0) { + // end of the list; skip all contents + return seenLiteral ? token : null; + } else { + // curly braces are not yet balanced; continue to the next token + break; + } default: // some other token than the expected ones; this is not a sequence of literals - return false; + return null; } } - return false; + return null; + } + + public boolean isDiscardingLiterals() { + return discardingLiteralsUntil != null; } @Override protected boolean isLanguageSpecificDiscarding() { - return discardingUsings || discardingNL || discardingLiterals || discardCurrent; + return discardingUsings || discardingNL || isDiscardingLiterals() || discardCurrent; } } } diff --git a/pmd-cs/src/test/java/net/sourceforge/pmd/cpd/CsTokenizerTest.java b/pmd-cs/src/test/java/net/sourceforge/pmd/cpd/CsTokenizerTest.java index 6b61c658bb..63c9cd5aee 100644 --- a/pmd-cs/src/test/java/net/sourceforge/pmd/cpd/CsTokenizerTest.java +++ b/pmd-cs/src/test/java/net/sourceforge/pmd/cpd/CsTokenizerTest.java @@ -100,6 +100,11 @@ public class CsTokenizerTest extends CpdTextComparisonTest { doTest("listOfNumbers", "_ignored", skipLiteralSequences()); } + @Test + public void testCSharp7And8Additions() { + doTest("csharp7And8Additions"); + } + private Properties ignoreUsings() { return properties(true, false); } diff --git a/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/comments.cs b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/comments.cs index 1757cf2afd..fe3d2575ce 100644 --- a/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/comments.cs +++ b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/comments.cs @@ -1,3 +1,5 @@ + /**/ +// the previous comment is an empty delimited comment and not a document comment class Foo { /// class X /* aaa diff --git a/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/comments.txt b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/comments.txt index 92214cb4e9..0134960679 100644 --- a/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/comments.txt +++ b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/comments.txt @@ -1,8 +1,8 @@ [Image] or [Truncated image[ Bcol Ecol -L1 +L3 [class] 1 5 [Foo] 7 9 [{] 11 11 -L6 +L8 [}] 2 2 EOF diff --git a/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/csharp7And8Additions.cs b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/csharp7And8Additions.cs new file mode 100644 index 0000000000..6d14774d75 --- /dev/null +++ b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/csharp7And8Additions.cs @@ -0,0 +1,26 @@ +#nullable enable +using System; +using System.Collections.Generic; + +class CSharp7And8Additions +{ + private static void Literals() + { + int x = 30_000_000; // digit separators + int b = 0b00101000; // boolean literal + } + + private static unsafe void DisplaySize() where T : unmanaged // unmanaged keyword + { + Console.WriteLine($"{typeof(T)} is unmanaged and its size is {sizeof(T)} bytes"); + } + + private static void Operators() + { + List? l = null; + (l ??= new List()).Add(5); // null-coalescing assignment operator + + var array = new int[] { 1, 2, 3, 4, 5 }; + var slice1 = array[2..^3]; // range operator + } +} diff --git a/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/csharp7And8Additions.txt b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/csharp7And8Additions.txt new file mode 100644 index 0000000000..5d689e8505 --- /dev/null +++ b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/csharp7And8Additions.txt @@ -0,0 +1,155 @@ + [Image] or [Truncated image[ Bcol Ecol +L1 + [#] 1 1 +L2 + [using] 1 5 + [System] 7 12 + [;] 13 13 +L3 + [using] 1 5 + [System] 7 12 + [.] 13 13 + [Collections] 14 24 + [.] 25 25 + [Generic] 26 32 + [;] 33 33 +L5 + [class] 1 5 + [CSharp7And8Additions] 7 26 +L6 + [{] 1 1 +L7 + [private] 5 11 + [static] 13 18 + [void] 20 23 + [Literals] 25 32 + [(] 33 33 + [)] 34 34 +L8 + [{] 5 5 +L9 + [int] 9 11 + [x] 13 13 + [=] 15 15 + [30_000_000] 17 26 + [;] 27 27 +L10 + [int] 9 11 + [b] 13 13 + [=] 15 15 + [0b00101000] 17 26 + [;] 27 27 +L11 + [}] 5 5 +L13 + [private] 5 11 + [static] 13 18 + [unsafe] 20 25 + [void] 27 30 + [DisplaySize] 32 42 + [<] 43 43 + [T] 44 44 + [>] 45 45 + [(] 46 46 + [)] 47 47 + [where] 49 53 + [T] 55 55 + [:] 57 57 + [unmanaged] 59 67 +L14 + [{] 5 5 +L15 + [Console] 9 15 + [.] 16 16 + [WriteLine] 17 25 + [(] 26 26 + [$"] 27 28 + [typeof] 30 35 + [(] 36 36 + [T] 37 37 + [)] 38 38 + [ is unmanaged and its size is ] 40 69 + [sizeof] 71 76 + [(] 77 77 + [T] 78 78 + [)] 79 79 + [ bytes] 81 86 + ["] 87 87 + [)] 88 88 + [;] 89 89 +L16 + [}] 5 5 +L18 + [private] 5 11 + [static] 13 18 + [void] 20 23 + [Operators] 25 33 + [(] 34 34 + [)] 35 35 +L19 + [{] 5 5 +L20 + [List] 9 12 + [<] 13 13 + [int] 14 16 + [>] 17 17 + [?] 18 18 + [l] 20 20 + [=] 22 22 + [null] 24 27 + [;] 28 28 +L21 + [(] 9 9 + [l] 10 10 + [??=] 12 14 + [new] 16 18 + [List] 20 23 + [<] 24 24 + [int] 25 27 + [>] 28 28 + [(] 29 29 + [)] 30 30 + [)] 31 31 + [.] 32 32 + [Add] 33 35 + [(] 36 36 + [5] 37 37 + [)] 38 38 + [;] 39 39 +L23 + [var] 9 11 + [array] 13 17 + [=] 19 19 + [new] 21 23 + [int] 25 27 + [\[] 28 28 + [\]] 29 29 + [{] 31 31 + [1] 33 33 + [,] 34 34 + [2] 36 36 + [,] 37 37 + [3] 39 39 + [,] 40 40 + [4] 42 42 + [,] 43 43 + [5] 45 45 + [}] 47 47 + [;] 48 48 +L24 + [var] 9 11 + [slice1] 13 18 + [=] 20 20 + [array] 22 26 + [\[] 27 27 + [2] 28 28 + [..] 29 30 + [^] 31 31 + [3] 32 32 + [\]] 33 33 + [;] 34 34 +L25 + [}] 5 5 +L26 + [}] 1 1 +EOF diff --git a/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/listOfNumbers.cs b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/listOfNumbers.cs index a3ceee1dca..6ebda7d3ff 100644 --- a/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/listOfNumbers.cs +++ b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/listOfNumbers.cs @@ -3,6 +3,22 @@ using System.Collections; using System.Collections.Generic; public class LongLists { List l = new List { - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + }; + byte[,] a = {1,2,3,4,5}; + byte[,] b = {{1,2},{3,4},{5,6}}; + int[,] c = { + 157, // decimal literal + 0377, // octal literal + 36_000_000, // literal with digit separators + 0x3fff, // hexadecimal literal + 0X3FFF, // same hexadecimal literal + 328u, // unsigned value + 0x7FFFFFL, // long value + 0776745ul, // unsigned long value + 18.46, // float + 18.46e0, // double with exponent + 18.46e1, // double with exponent + 0b000001, // binary literal }; } diff --git a/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/listOfNumbers.txt b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/listOfNumbers.txt index 17d20f914d..765029a808 100644 --- a/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/listOfNumbers.txt +++ b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/listOfNumbers.txt @@ -36,309 +36,402 @@ L5 [>] 33 33 [{] 35 35 L6 - [0] 6 6 - [,] 7 7 - [0] 8 8 - [,] 9 9 - [0] 10 10 - [,] 11 11 - [0] 12 12 - [,] 13 13 - [0] 14 14 - [,] 15 15 - [0] 16 16 - [,] 17 17 - [0] 18 18 - [,] 19 19 - [0] 20 20 - [,] 21 21 - [0] 22 22 - [,] 23 23 - [0] 24 24 - [,] 25 25 - [0] 26 26 - [,] 27 27 - [0] 28 28 - [,] 29 29 - [0] 30 30 - [,] 31 31 - [0] 32 32 - [,] 33 33 - [0] 34 34 - [,] 35 35 - [0] 36 36 - [,] 37 37 - [0] 38 38 - [,] 39 39 - [0] 40 40 - [,] 41 41 - [0] 42 42 - [,] 43 43 - [0] 44 44 - [,] 45 45 - [0] 46 46 - [,] 47 47 - [0] 48 48 - [,] 49 49 - [0] 50 50 - [,] 51 51 - [0] 52 52 - [,] 53 53 - [0] 54 54 - [,] 55 55 - [0] 56 56 - [,] 57 57 - [0] 58 58 - [,] 59 59 - [0] 60 60 - [,] 61 61 - [0] 62 62 - [,] 63 63 - [0] 64 64 - [,] 65 65 - [0] 66 66 - [,] 67 67 - [0] 68 68 - [,] 69 69 - [0] 70 70 - [,] 71 71 - [0] 72 72 - [,] 73 73 - [0] 74 74 - [,] 75 75 - [0] 76 76 - [,] 77 77 - [0] 78 78 - [,] 79 79 - [0] 80 80 - [,] 81 81 - [0] 82 82 - [,] 83 83 - [0] 84 84 - [,] 85 85 - [0] 86 86 - [,] 87 87 - [0] 88 88 - [,] 89 89 - [0] 90 90 - [,] 91 91 - [0] 92 92 - [,] 93 93 - [0] 94 94 - [,] 95 95 - [0] 96 96 - [,] 97 97 - [0] 98 98 - [,] 99 99 - [0] 100 100 - [,] 101 101 - [0] 102 102 - [,] 103 103 - [0] 104 104 - [,] 105 105 - [0] 106 106 - [,] 107 107 - [0] 108 108 - [,] 109 109 - [0] 110 110 - [,] 111 111 - [0] 112 112 - [,] 113 113 - [0] 114 114 - [,] 115 115 - [0] 116 116 - [,] 117 117 - [0] 118 118 - [,] 119 119 - [0] 120 120 - [,] 121 121 - [0] 122 122 - [,] 123 123 - [0] 124 124 - [,] 125 125 - [0] 126 126 - [,] 127 127 - [0] 128 128 - [,] 129 129 - [0] 130 130 - [,] 131 131 - [0] 132 132 - [,] 133 133 - [0] 134 134 - [,] 135 135 - [0] 136 136 - [,] 137 137 - [0] 138 138 - [,] 139 139 - [0] 140 140 - [,] 141 141 - [0] 142 142 - [,] 143 143 - [0] 144 144 - [,] 145 145 - [0] 146 146 - [,] 147 147 - [0] 148 148 - [,] 149 149 - [0] 150 150 - [,] 151 151 - [0] 152 152 - [,] 153 153 - [0] 154 154 - [,] 155 155 - [0] 156 156 - [,] 157 157 - [0] 158 158 - [,] 159 159 - [0] 160 160 - [,] 161 161 - [0] 162 162 - [,] 163 163 - [0] 164 164 - [,] 165 165 - [0] 166 166 - [,] 167 167 - [0] 168 168 - [,] 169 169 - [0] 170 170 - [,] 171 171 - [0] 172 172 - [,] 173 173 - [0] 174 174 - [,] 175 175 - [0] 176 176 - [,] 177 177 - [0] 178 178 - [,] 179 179 - [0] 180 180 - [,] 181 181 - [0] 182 182 - [,] 183 183 - [0] 184 184 - [,] 185 185 - [0] 186 186 - [,] 187 187 - [0] 188 188 - [,] 189 189 - [0] 190 190 - [,] 191 191 - [0] 192 192 - [,] 193 193 - [0] 194 194 - [,] 195 195 - [0] 196 196 - [,] 197 197 - [0] 198 198 - [,] 199 199 - [0] 200 200 - [,] 201 201 - [0] 202 202 - [,] 203 203 - [0] 204 204 - [,] 205 205 - [0] 206 206 - [,] 207 207 - [0] 208 208 - [,] 209 209 - [0] 210 210 - [,] 211 211 - [0] 212 212 - [,] 213 213 - [0] 214 214 - [,] 215 215 - [0] 216 216 - [,] 217 217 - [0] 218 218 - [,] 219 219 - [0] 220 220 - [,] 221 221 - [0] 222 222 - [,] 223 223 - [0] 224 224 - [,] 225 225 - [0] 226 226 - [,] 227 227 - [0] 228 228 - [,] 229 229 - [0] 230 230 - [,] 231 231 - [0] 232 232 - [,] 233 233 - [0] 234 234 - [,] 235 235 - [0] 236 236 - [,] 237 237 - [0] 238 238 - [,] 239 239 - [0] 240 240 - [,] 241 241 - [0] 242 242 - [,] 243 243 - [0] 244 244 - [,] 245 245 - [0] 246 246 - [,] 247 247 - [0] 248 248 - [,] 249 249 - [0] 250 250 - [,] 251 251 - [0] 252 252 - [,] 253 253 - [0] 254 254 - [,] 255 255 - [0] 256 256 - [,] 257 257 - [0] 258 258 - [,] 259 259 - [0] 260 260 - [,] 261 261 - [0] 262 262 - [,] 263 263 - [0] 264 264 - [,] 265 265 - [0] 266 266 - [,] 267 267 - [0] 268 268 - [,] 269 269 - [0] 270 270 - [,] 271 271 - [0] 272 272 - [,] 273 273 - [0] 274 274 - [,] 275 275 - [0] 276 276 - [,] 277 277 - [0] 278 278 - [,] 279 279 - [0] 280 280 - [,] 281 281 - [0] 282 282 - [,] 283 283 - [0] 284 284 - [,] 285 285 - [0] 286 286 - [,] 287 287 - [0] 288 288 - [,] 289 289 - [0] 290 290 - [,] 291 291 - [0] 292 292 - [,] 293 293 - [0] 294 294 - [,] 295 295 - [0] 296 296 - [,] 297 297 - [0] 298 298 - [,] 299 299 - [0] 300 300 - [,] 301 301 - [0] 302 302 - [,] 303 303 - [0] 304 304 - [,] 305 305 + [0] 7 7 + [,] 8 8 + [0] 9 9 + [,] 10 10 + [0] 11 11 + [,] 12 12 + [0] 13 13 + [,] 14 14 + [0] 15 15 + [,] 16 16 + [0] 17 17 + [,] 18 18 + [0] 19 19 + [,] 20 20 + [0] 21 21 + [,] 22 22 + [0] 23 23 + [,] 24 24 + [0] 25 25 + [,] 26 26 + [0] 27 27 + [,] 28 28 + [0] 29 29 + [,] 30 30 + [0] 31 31 + [,] 32 32 + [0] 33 33 + [,] 34 34 + [0] 35 35 + [,] 36 36 + [0] 37 37 + [,] 38 38 + [0] 39 39 + [,] 40 40 + [0] 41 41 + [,] 42 42 + [0] 43 43 + [,] 44 44 + [0] 45 45 + [,] 46 46 + [0] 47 47 + [,] 48 48 + [0] 49 49 + [,] 50 50 + [0] 51 51 + [,] 52 52 + [0] 53 53 + [,] 54 54 + [0] 55 55 + [,] 56 56 + [0] 57 57 + [,] 58 58 + [0] 59 59 + [,] 60 60 + [0] 61 61 + [,] 62 62 + [0] 63 63 + [,] 64 64 + [0] 65 65 + [,] 66 66 + [0] 67 67 + [,] 68 68 + [0] 69 69 + [,] 70 70 + [0] 71 71 + [,] 72 72 + [0] 73 73 + [,] 74 74 + [0] 75 75 + [,] 76 76 + [0] 77 77 + [,] 78 78 + [0] 79 79 + [,] 80 80 + [0] 81 81 + [,] 82 82 + [0] 83 83 + [,] 84 84 + [0] 85 85 + [,] 86 86 + [0] 87 87 + [,] 88 88 + [0] 89 89 + [,] 90 90 + [0] 91 91 + [,] 92 92 + [0] 93 93 + [,] 94 94 + [0] 95 95 + [,] 96 96 + [0] 97 97 + [,] 98 98 + [0] 99 99 + [,] 100 100 + [0] 101 101 + [,] 102 102 + [0] 103 103 + [,] 104 104 + [0] 105 105 + [,] 106 106 + [0] 107 107 + [,] 108 108 + [0] 109 109 + [,] 110 110 + [0] 111 111 + [,] 112 112 + [0] 113 113 + [,] 114 114 + [0] 115 115 + [,] 116 116 + [0] 117 117 + [,] 118 118 + [0] 119 119 + [,] 120 120 + [0] 121 121 + [,] 122 122 + [0] 123 123 + [,] 124 124 + [0] 125 125 + [,] 126 126 + [0] 127 127 + [,] 128 128 + [0] 129 129 + [,] 130 130 + [0] 131 131 + [,] 132 132 + [0] 133 133 + [,] 134 134 + [0] 135 135 + [,] 136 136 + [0] 137 137 + [,] 138 138 + [0] 139 139 + [,] 140 140 + [0] 141 141 + [,] 142 142 + [0] 143 143 + [,] 144 144 + [0] 145 145 + [,] 146 146 + [0] 147 147 + [,] 148 148 + [0] 149 149 + [,] 150 150 + [0] 151 151 + [,] 152 152 + [0] 153 153 + [,] 154 154 + [0] 155 155 + [,] 156 156 + [0] 157 157 + [,] 158 158 + [0] 159 159 + [,] 160 160 + [0] 161 161 + [,] 162 162 + [0] 163 163 + [,] 164 164 + [0] 165 165 + [,] 166 166 + [0] 167 167 + [,] 168 168 + [0] 169 169 + [,] 170 170 + [0] 171 171 + [,] 172 172 + [0] 173 173 + [,] 174 174 + [0] 175 175 + [,] 176 176 + [0] 177 177 + [,] 178 178 + [0] 179 179 + [,] 180 180 + [0] 181 181 + [,] 182 182 + [0] 183 183 + [,] 184 184 + [0] 185 185 + [,] 186 186 + [0] 187 187 + [,] 188 188 + [0] 189 189 + [,] 190 190 + [0] 191 191 + [,] 192 192 + [0] 193 193 + [,] 194 194 + [0] 195 195 + [,] 196 196 + [0] 197 197 + [,] 198 198 + [0] 199 199 + [,] 200 200 + [0] 201 201 + [,] 202 202 + [0] 203 203 + [,] 204 204 + [0] 205 205 + [,] 206 206 + [0] 207 207 + [,] 208 208 + [0] 209 209 + [,] 210 210 + [0] 211 211 + [,] 212 212 + [0] 213 213 + [,] 214 214 + [0] 215 215 + [,] 216 216 + [0] 217 217 + [,] 218 218 + [0] 219 219 + [,] 220 220 + [0] 221 221 + [,] 222 222 + [0] 223 223 + [,] 224 224 + [0] 225 225 + [,] 226 226 + [0] 227 227 + [,] 228 228 + [0] 229 229 + [,] 230 230 + [0] 231 231 + [,] 232 232 + [0] 233 233 + [,] 234 234 + [0] 235 235 + [,] 236 236 + [0] 237 237 + [,] 238 238 + [0] 239 239 + [,] 240 240 + [0] 241 241 + [,] 242 242 + [0] 243 243 + [,] 244 244 + [0] 245 245 + [,] 246 246 + [0] 247 247 + [,] 248 248 + [0] 249 249 + [,] 250 250 + [0] 251 251 + [,] 252 252 + [0] 253 253 + [,] 254 254 + [0] 255 255 + [,] 256 256 + [0] 257 257 + [,] 258 258 + [0] 259 259 + [,] 260 260 + [0] 261 261 + [,] 262 262 + [0] 263 263 + [,] 264 264 + [0] 265 265 + [,] 266 266 + [0] 267 267 + [,] 268 268 + [0] 269 269 + [,] 270 270 + [0] 271 271 + [,] 272 272 + [0] 273 273 + [,] 274 274 + [0] 275 275 + [,] 276 276 + [0] 277 277 + [,] 278 278 + [0] 279 279 + [,] 280 280 + [0] 281 281 + [,] 282 282 + [0] 283 283 + [,] 284 284 + [0] 285 285 + [,] 286 286 + [0] 287 287 + [,] 288 288 + [0] 289 289 + [,] 290 290 + [0] 291 291 + [,] 292 292 + [0] 293 293 + [,] 294 294 + [0] 295 295 + [,] 296 296 + [0] 297 297 + [,] 298 298 + [0] 299 299 + [,] 300 300 + [0] 301 301 + [,] 302 302 + [0] 303 303 + [,] 304 304 + [0] 305 305 + [,] 306 306 L7 [}] 5 5 [;] 6 6 L8 + [byte] 5 8 + [\[] 9 9 + [,] 10 10 + [\]] 11 11 + [a] 13 13 + [=] 15 15 + [{] 17 17 + [1] 18 18 + [,] 19 19 + [2] 20 20 + [,] 21 21 + [3] 22 22 + [,] 23 23 + [4] 24 24 + [,] 25 25 + [5] 26 26 + [}] 27 27 + [;] 28 28 +L9 + [byte] 5 8 + [\[] 9 9 + [,] 10 10 + [\]] 11 11 + [b] 13 13 + [=] 15 15 + [{] 17 17 + [{] 18 18 + [1] 19 19 + [,] 20 20 + [2] 21 21 + [}] 22 22 + [,] 23 23 + [{] 24 24 + [3] 25 25 + [,] 26 26 + [4] 27 27 + [}] 28 28 + [,] 29 29 + [{] 30 30 + [5] 31 31 + [,] 32 32 + [6] 33 33 + [}] 34 34 + [}] 35 35 + [;] 36 36 +L10 + [int] 5 7 + [\[] 8 8 + [,] 9 9 + [\]] 10 10 + [c] 12 12 + [=] 14 14 + [{] 16 16 +L11 + [157] 7 9 + [,] 10 10 +L12 + [0377] 7 10 + [,] 11 11 +L13 + [36_000_000] 7 16 + [,] 17 17 +L14 + [0x3fff] 7 12 + [,] 13 13 +L15 + [0X3FFF] 7 12 + [,] 13 13 +L16 + [328u] 7 10 + [,] 11 11 +L17 + [0x7FFFFFL] 7 15 + [,] 16 16 +L18 + [0776745ul] 7 15 + [,] 16 16 +L19 + [18.46] 7 11 + [,] 12 12 +L20 + [18.46e0] 7 13 + [,] 14 14 +L21 + [18.46e1] 7 13 + [,] 14 14 +L22 + [0b000001] 7 14 + [,] 15 15 +L23 + [}] 5 5 + [;] 6 6 +L24 [}] 1 1 EOF diff --git a/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/listOfNumbers_ignored.txt b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/listOfNumbers_ignored.txt index 03f835fdca..79b782b67d 100644 --- a/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/listOfNumbers_ignored.txt +++ b/pmd-cs/src/test/resources/net/sourceforge/pmd/lang/cs/cpd/testdata/listOfNumbers_ignored.txt @@ -37,5 +37,30 @@ L5 L7 [;] 6 6 L8 + [byte] 5 8 + [\[] 9 9 + [,] 10 10 + [\]] 11 11 + [a] 13 13 + [=] 15 15 + [;] 28 28 +L9 + [byte] 5 8 + [\[] 9 9 + [,] 10 10 + [\]] 11 11 + [b] 13 13 + [=] 15 15 + [;] 36 36 +L10 + [int] 5 7 + [\[] 8 8 + [,] 9 9 + [\]] 10 10 + [c] 12 12 + [=] 14 14 +L23 + [;] 6 6 +L24 [}] 1 1 EOF diff --git a/pmd-dart/pom.xml b/pmd-dart/pom.xml index 34d8fb11be..162da6a369 100644 --- a/pmd-dart/pom.xml +++ b/pmd-dart/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-dist/pom.xml b/pmd-dist/pom.xml index 5d1078311b..277fb54ec8 100644 --- a/pmd-dist/pom.xml +++ b/pmd-dist/pom.xml @@ -8,7 +8,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ @@ -148,11 +148,6 @@ pmd-jsp ${project.version} - - net.sourceforge.pmd - pmd-visualforce - ${project.version} - net.sourceforge.pmd pmd-kotlin @@ -264,6 +259,11 @@ pmd-ui ${pmd-designer.version} + + net.sourceforge.pmd + pmd-visualforce + ${project.version} + diff --git a/pmd-dist/src/test/java/net/sourceforge/pmd/it/AllRulesIT.java b/pmd-dist/src/test/java/net/sourceforge/pmd/it/AllRulesIT.java index 91b96df299..8ba9e0533f 100644 --- a/pmd-dist/src/test/java/net/sourceforge/pmd/it/AllRulesIT.java +++ b/pmd-dist/src/test/java/net/sourceforge/pmd/it/AllRulesIT.java @@ -22,9 +22,9 @@ public class AllRulesIT extends AbstractBinaryDistributionTest { @Parameters public static Iterable languagesToTest() { if (PMDExecutor.isJava7Test()) { - // note: apex and scala require java8 + // note: apex, scala, and visualforce require java8 return Arrays.asList("java", "javascript", "jsp", "modelica", - "plsql", "pom", "visualforce", "velocitytemplate", "xml", "xsl"); + "plsql", "pom", "velocitytemplate", "xml", "xsl"); } // note: scala and wsdl have no rules return Arrays.asList("java", "apex", "javascript", "jsp", "modelica", diff --git a/pmd-dist/src/test/java/net/sourceforge/pmd/it/BinaryDistributionIT.java b/pmd-dist/src/test/java/net/sourceforge/pmd/it/BinaryDistributionIT.java index 755e2085b1..d5bff3f134 100644 --- a/pmd-dist/src/test/java/net/sourceforge/pmd/it/BinaryDistributionIT.java +++ b/pmd-dist/src/test/java/net/sourceforge/pmd/it/BinaryDistributionIT.java @@ -25,10 +25,10 @@ public class BinaryDistributionIT extends AbstractBinaryDistributionTest { private static final String SUPPORTED_LANGUAGES_PMD; static { - // note: apex and scala require java8 + // note: apex, visualforce, and scala require java8 if (PMDExecutor.isJava7Test()) { - SUPPORTED_LANGUAGES_CPD = "Supported languages: [cpp, cs, dart, ecmascript, fortran, go, groovy, java, jsp, kotlin, lua, matlab, modelica, objectivec, perl, php, plsql, python, ruby, swift, vf, xml]"; - SUPPORTED_LANGUAGES_PMD = "ecmascript, java, jsp, modelica, plsql, pom, vf, vm, wsdl, xml, xsl"; + SUPPORTED_LANGUAGES_CPD = "Supported languages: [cpp, cs, dart, ecmascript, fortran, go, groovy, java, jsp, kotlin, lua, matlab, modelica, objectivec, perl, php, plsql, python, ruby, swift, xml]"; + SUPPORTED_LANGUAGES_PMD = "ecmascript, java, jsp, modelica, plsql, pom, vm, wsdl, xml, xsl"; } else { SUPPORTED_LANGUAGES_CPD = "Supported languages: [apex, cpp, cs, dart, ecmascript, fortran, go, groovy, java, jsp, kotlin, lua, matlab, modelica, objectivec, perl, php, plsql, python, ruby, scala, swift, vf, xml]"; SUPPORTED_LANGUAGES_PMD = "apex, ecmascript, java, jsp, modelica, plsql, pom, scala, vf, vm, wsdl, xml, xsl"; diff --git a/pmd-doc/pom.xml b/pmd-doc/pom.xml index 25621b7967..220e8cc4f6 100644 --- a/pmd-doc/pom.xml +++ b/pmd-doc/pom.xml @@ -8,7 +8,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/GenerateRuleDocsCmd.java b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/GenerateRuleDocsCmd.java index 2256a21d18..1658262fb3 100644 --- a/pmd-doc/src/main/java/net/sourceforge/pmd/docs/GenerateRuleDocsCmd.java +++ b/pmd-doc/src/main/java/net/sourceforge/pmd/docs/GenerateRuleDocsCmd.java @@ -13,24 +13,20 @@ import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import java.util.regex.Pattern; import org.apache.commons.io.FilenameUtils; -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; public final class GenerateRuleDocsCmd { private GenerateRuleDocsCmd() { // Utility class } - public static void main(String[] args) throws RuleSetNotFoundException { + public static void main(String[] args) throws IOException { if (args.length != 1) { System.err.println("One argument is required: The base directory of the module pmd-doc."); System.exit(1); @@ -41,8 +37,7 @@ public final class GenerateRuleDocsCmd { System.out.println("Generating docs into " + output); // important: use a RuleSetFactory that includes all rules, e.g. deprecated rule references - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.createFactory(RulePriority.LOW, false, false, true); - Iterator registeredRuleSets = ruleSetFactory.getRegisteredRuleSets(); + List registeredRuleSets = new RuleSetLoader().getStandardRuleSets(); List additionalRulesets = findAdditionalRulesets(output); RuleDocGenerator generator = new RuleDocGenerator(new DefaultFileWriter(), output); 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 f62400b1a9..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 @@ -17,7 +17,6 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -29,6 +28,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; @@ -36,9 +36,8 @@ import org.apache.commons.text.StringEscapeUtils; import net.sourceforge.pmd.Rule; import net.sourceforge.pmd.RuleSet; -import net.sourceforge.pmd.RuleSetFactory; -import net.sourceforge.pmd.RuleSetNotFoundException; -import net.sourceforge.pmd.RulesetsFactoryUtils; +import net.sourceforge.pmd.RuleSetLoadException; +import net.sourceforge.pmd.RuleSetLoader; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.rule.RuleReference; import net.sourceforge.pmd.lang.rule.XPathRule; @@ -91,21 +90,16 @@ public class RuleDocGenerator { } } - public void generate(Iterator registeredRulesets, List additionalRulesets) { + public void generate(List registeredRulesets, List additionalRulesets) throws IOException { Map> sortedRulesets; Map> sortedAdditionalRulesets; - try { - sortedRulesets = sortRulesets(registeredRulesets); - sortedAdditionalRulesets = sortRulesets(resolveAdditionalRulesets(additionalRulesets)); - determineRuleClassSourceFiles(sortedRulesets); - generateLanguageIndex(sortedRulesets, sortedAdditionalRulesets); - generateRuleSetIndex(sortedRulesets); + sortedRulesets = sortRulesets(registeredRulesets); + sortedAdditionalRulesets = sortRulesets(resolveAdditionalRulesets(additionalRulesets)); + determineRuleClassSourceFiles(sortedRulesets); + generateLanguageIndex(sortedRulesets, sortedAdditionalRulesets); + generateRuleSetIndex(sortedRulesets); - generateSidebar(sortedRulesets); - - } catch (RuleSetNotFoundException | IOException e) { - throw new RuntimeException(e); - } + generateSidebar(sortedRulesets); } private void generateSidebar(Map> sortedRulesets) throws IOException { @@ -113,53 +107,40 @@ public class RuleDocGenerator { generator.generateSidebar(sortedRulesets); } - private Iterator resolveAdditionalRulesets(List additionalRulesets) throws RuleSetNotFoundException { + private List resolveAdditionalRulesets(List additionalRulesets) { if (additionalRulesets == null) { - return Collections.emptyIterator(); + return Collections.emptyList(); } List rulesets = new ArrayList<>(); - RuleSetFactory ruleSetFactory = RulesetsFactoryUtils.defaultFactory(); + RuleSetLoader ruleSetLoader = new RuleSetLoader(); for (String filename : additionalRulesets) { try { // do not take rulesets from pmd-test or pmd-core if (!filename.contains("pmd-test") && !filename.contains("pmd-core")) { - rulesets.add(ruleSetFactory.createRuleSet(filename)); + rulesets.add(ruleSetLoader.loadFromResource(filename)); } else { LOG.fine("Ignoring ruleset " + filename); } - } catch (IllegalArgumentException e) { + } catch (RuleSetLoadException e) { // ignore rulesets, we can't read LOG.log(Level.WARNING, "ruleset file " + filename + " ignored (" + e.getMessage() + ")", e); } } - return rulesets.iterator(); + return rulesets; } private Path getAbsoluteOutputPath(String filename) { return root.resolve(FilenameUtils.normalize(filename)); } - private Map> sortRulesets(Iterator rulesets) throws RuleSetNotFoundException { - SortedMap> rulesetsByLanguage = new TreeMap<>(); - - while (rulesets.hasNext()) { - RuleSet ruleset = rulesets.next(); - Language language = getRuleSetLanguage(ruleset); - - if (!rulesetsByLanguage.containsKey(language)) { - rulesetsByLanguage.put(language, new ArrayList()); - } - rulesetsByLanguage.get(language).add(ruleset); - } + private Map> sortRulesets(List rulesets) { + SortedMap> rulesetsByLanguage = rulesets.stream().collect(Collectors.groupingBy(RuleDocGenerator::getRuleSetLanguage, + TreeMap::new, + Collectors.toCollection(ArrayList::new))); for (List rulesetsOfOneLanguage : rulesetsByLanguage.values()) { - Collections.sort(rulesetsOfOneLanguage, new Comparator() { - @Override - public int compare(RuleSet o1, RuleSet o2) { - return o1.getName().compareToIgnoreCase(o2.getName()); - } - }); + rulesetsOfOneLanguage.sort((o1, o2) -> o1.getName().compareToIgnoreCase(o2.getName())); } return rulesetsByLanguage; } 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 5e7561cdf3..23d91a043d 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 @@ -65,7 +65,7 @@ public class RuleDocGeneratorTest { RuleSetFactory rsf = RulesetsFactoryUtils.createFactory(RulePriority.LOW, false, false, true); RuleSet ruleset = rsf.createRuleSet("rulesets/ruledoctest/sample.xml"); - generator.generate(Arrays.asList(ruleset).iterator(), + generator.generate(Arrays.asList(ruleset), Arrays.asList( "rulesets/ruledoctest/sample-deprecated.xml", "rulesets/ruledoctest/other-ruleset.xml")); diff --git a/pmd-fortran/pom.xml b/pmd-fortran/pom.xml index 9787233fa4..a7849a888b 100644 --- a/pmd-fortran/pom.xml +++ b/pmd-fortran/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-go/pom.xml b/pmd-go/pom.xml index f499309565..5cb6b21da1 100644 --- a/pmd-go/pom.xml +++ b/pmd-go/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-groovy/pom.xml b/pmd-groovy/pom.xml index de6f4770b0..81280f254f 100644 --- a/pmd-groovy/pom.xml +++ b/pmd-groovy/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-java/pom.xml b/pmd-java/pom.xml index f3f22fc26f..30275fa027 100644 --- a/pmd-java/pom.xml +++ b/pmd-java/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-java/src/main/ant/alljavacc.xml b/pmd-java/src/main/ant/alljavacc.xml index ee9bcfe7b8..9e74cd4db6 100644 --- a/pmd-java/src/main/ant/alljavacc.xml +++ b/pmd-java/src/main/ant/alljavacc.xml @@ -28,15 +28,23 @@ - + + + + + - + + + + + + + diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/LiteralsFirstInComparisonsRule.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/LiteralsFirstInComparisonsRule.java index 3419dd8d65..d89bcedf5e 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/LiteralsFirstInComparisonsRule.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/LiteralsFirstInComparisonsRule.java @@ -8,16 +8,20 @@ import java.util.List; import net.sourceforge.pmd.lang.java.ast.ASTArgumentList; import net.sourceforge.pmd.lang.java.ast.ASTArguments; +import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceBody; +import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceBodyDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTConditionalAndExpression; import net.sourceforge.pmd.lang.java.ast.ASTConditionalOrExpression; import net.sourceforge.pmd.lang.java.ast.ASTEqualityExpression; import net.sourceforge.pmd.lang.java.ast.ASTExpression; +import net.sourceforge.pmd.lang.java.ast.ASTFieldDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTLiteral; import net.sourceforge.pmd.lang.java.ast.ASTName; import net.sourceforge.pmd.lang.java.ast.ASTNullLiteral; import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression; import net.sourceforge.pmd.lang.java.ast.ASTPrimaryPrefix; import net.sourceforge.pmd.lang.java.ast.ASTPrimarySuffix; +import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclarator; import net.sourceforge.pmd.lang.java.ast.JavaNode; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; @@ -123,20 +127,28 @@ public class LiteralsFirstInComparisonsRule extends AbstractJavaRule { private boolean isStringLiteralFirstArgumentOfSuffix(ASTPrimarySuffix primarySuffix) { try { - JavaNode firstArg = getFirstArgument(primarySuffix); - return isStringLiteral(firstArg); + JavaNode firstLiteralArg = getFirstLiteralArgument(primarySuffix); + JavaNode firstNameArg = getFirstNameArgument(primarySuffix); + return isStringLiteral(firstLiteralArg) || isConstantString(firstNameArg); } catch (NullPointerException e) { return false; } } - private JavaNode getFirstArgument(ASTPrimarySuffix primarySuffix) { + private JavaNode getFirstLiteralArgument(ASTPrimarySuffix primarySuffix) { + return getArgumentPrimaryPrefix(primarySuffix).getFirstChildOfType(ASTLiteral.class); + } + + private JavaNode getFirstNameArgument(ASTPrimarySuffix primarySuffix) { + return getArgumentPrimaryPrefix(primarySuffix).getFirstChildOfType(ASTName.class); + } + + private JavaNode getArgumentPrimaryPrefix(ASTPrimarySuffix primarySuffix) { ASTArguments arguments = primarySuffix.getFirstChildOfType(ASTArguments.class); ASTArgumentList argumentList = arguments.getFirstChildOfType(ASTArgumentList.class); ASTExpression expression = argumentList.getFirstChildOfType(ASTExpression.class); ASTPrimaryExpression primaryExpression = expression.getFirstChildOfType(ASTPrimaryExpression.class); - ASTPrimaryPrefix primaryPrefix = primaryExpression.getFirstChildOfType(ASTPrimaryPrefix.class); - return primaryPrefix.getFirstChildOfType(ASTLiteral.class); + return primaryExpression.getFirstChildOfType(ASTPrimaryPrefix.class); } private boolean isStringLiteral(JavaNode node) { @@ -147,6 +159,25 @@ public class LiteralsFirstInComparisonsRule extends AbstractJavaRule { return false; } + private boolean isConstantString(JavaNode node) { + if (node instanceof ASTName) { + ASTName name = (ASTName) node; + ASTClassOrInterfaceBody classBody = name.getFirstParentOfType(ASTClassOrInterfaceBody.class); + ASTClassOrInterfaceBodyDeclaration classOrInterfaceBodyDeclaration = classBody.getFirstChildOfType(ASTClassOrInterfaceBodyDeclaration.class); + List fieldDeclarations = classOrInterfaceBodyDeclaration.findChildrenOfType(ASTFieldDeclaration.class); + for (ASTFieldDeclaration fieldDeclaration : fieldDeclarations) { + ASTVariableDeclarator declaration = fieldDeclaration.getFirstChildOfType(ASTVariableDeclarator.class); + if (declaration.getName().equals(name.getImage()) + && String.class.equals(declaration.getType()) + && fieldDeclaration.isFinal() + && fieldDeclaration.isStatic()) { + return true; + } + } + } + return false; + } + private boolean isNotWithinNullComparison(ASTPrimaryExpression node) { return !isWithinNullComparison(node); } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/MethodNamingConventionsRule.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/MethodNamingConventionsRule.java index 46c74a8568..0183e2889c 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/MethodNamingConventionsRule.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/MethodNamingConventionsRule.java @@ -22,7 +22,7 @@ import net.sourceforge.pmd.properties.PropertyDescriptor; public class MethodNamingConventionsRule extends AbstractNamingConventionRule { - private static final Map DESCRIPTOR_TO_DISPLAY_NAME = new HashMap<>(); + private final Map descriptorToDisplayName = new HashMap<>(); @Deprecated private static final BooleanProperty CHECK_NATIVE_METHODS_DESCRIPTOR = new BooleanProperty("checkNativeMethods", @@ -118,7 +118,7 @@ public class MethodNamingConventionsRule extends AbstractNamingConventionRule descriptor) { - return DESCRIPTOR_TO_DISPLAY_NAME.get(descriptor.name()); + return descriptorToDisplayName.get(descriptor.name()); } } diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/ExcludeLinesTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/ExcludeLinesTest.java index 5704ef0951..2d68f04ba7 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/ExcludeLinesTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/ExcludeLinesTest.java @@ -41,7 +41,7 @@ public class ExcludeLinesTest extends RuleTst { ctx.setReport(r); ctx.setSourceCodeFile(new File("n/a")); ctx.setLanguageVersion(LanguageRegistry.getLanguage(JavaLanguageModule.NAME).getDefaultVersion()); - RuleSet rules = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(rule); + RuleSet rules = RuleSet.forSingleRule(rule); p.getSourceCodeProcessor().processSourceCode(new StringReader(TEST3), new RuleSets(rules), ctx); assertTrue(r.isEmpty()); assertEquals(r.getSuppressedRuleViolations().size(), 1); 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 3401199005..23de5ce3de 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 @@ -7,16 +7,15 @@ package net.sourceforge.pmd.lang.java; import org.junit.Assert; import org.junit.Test; -import net.sourceforge.pmd.RulePriority; import net.sourceforge.pmd.RuleSet; import net.sourceforge.pmd.RuleSetFactory; -import net.sourceforge.pmd.util.ResourceLoader; +import net.sourceforge.pmd.RuleSetLoader; public class PMD5RulesetTest { @Test public void loadRuleset() throws Exception { - RuleSetFactory ruleSetFactory = new RuleSetFactory(new ResourceLoader(), RulePriority.LOW, true, true); + RuleSetFactory ruleSetFactory = new RuleSetLoader().toFactory(); RuleSet ruleset = ruleSetFactory.createRuleSet("net/sourceforge/pmd/lang/java/pmd5ruleset.xml"); Assert.assertNotNull(ruleset); Assert.assertNull(ruleset.getRuleByName("GuardLogStatementJavaUtil")); 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 816c98b918..16c3142ab9 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 @@ -14,11 +14,10 @@ import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.SystemErrRule; -import net.sourceforge.pmd.RulePriority; import net.sourceforge.pmd.RuleSet; import net.sourceforge.pmd.RuleSetFactory; +import net.sourceforge.pmd.RuleSetLoader; import net.sourceforge.pmd.RuleSetNotFoundException; -import net.sourceforge.pmd.util.ResourceLoader; public class QuickstartRulesetTest { @@ -50,7 +49,7 @@ public class QuickstartRulesetTest { } }); - RuleSetFactory ruleSetFactory = new RuleSetFactory(new ResourceLoader(), RulePriority.LOW, true, false); + RuleSetFactory ruleSetFactory = new RuleSetLoader().enableCompatibility(false).toFactory(); RuleSet quickstart = ruleSetFactory.createRuleSet("rulesets/java/quickstart.xml"); Assert.assertFalse(quickstart.getRules().isEmpty()); } diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/metrics/xpath/XPathMetricFunctionTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/metrics/xpath/XPathMetricFunctionTest.java index 2dc75f74cc..1114a434ee 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/metrics/xpath/XPathMetricFunctionTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/metrics/xpath/XPathMetricFunctionTest.java @@ -21,7 +21,6 @@ import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.RuleSet; import net.sourceforge.pmd.RuleSets; import net.sourceforge.pmd.RuleViolation; -import net.sourceforge.pmd.RulesetsFactoryUtils; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.java.JavaLanguageModule; import net.sourceforge.pmd.lang.java.xpath.MetricFunction; @@ -57,7 +56,7 @@ public class XPathMetricFunctionTest { ctx.setReport(report); ctx.setSourceCodeFile(new File("n/a")); ctx.setIgnoreExceptions(false); // for test, we want immediate exceptions thrown and not collect them - RuleSet rules = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(rule); + RuleSet rules = RuleSet.forSingleRule(rule); p.getSourceCodeProcessor().processSourceCode(new StringReader(code), new RuleSets(rules), ctx); return report.iterator(); } diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/XPathRuleTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/XPathRuleTest.java index 90e01fabe5..e563974933 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/XPathRuleTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/XPathRuleTest.java @@ -22,7 +22,6 @@ import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.RuleSet; import net.sourceforge.pmd.RuleSets; import net.sourceforge.pmd.RuleViolation; -import net.sourceforge.pmd.RulesetsFactoryUtils; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.LanguageVersion; import net.sourceforge.pmd.lang.Parser; @@ -206,7 +205,7 @@ public class XPathRuleTest extends RuleTst { Report report = new Report(); ctx.setReport(report); ctx.setSourceCodeFile(new File("n/a")); - RuleSet rules = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(r); + RuleSet rules = RuleSet.forSingleRule(r); p.getSourceCodeProcessor().processSourceCode(new StringReader(test), new RuleSets(rules), ctx); return report; } diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/bestpractices/xml/LiteralsFirstInComparisons.xml b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/bestpractices/xml/LiteralsFirstInComparisons.xml index f76ba2658f..903bb45279 100644 --- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/bestpractices/xml/LiteralsFirstInComparisons.xml +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/bestpractices/xml/LiteralsFirstInComparisons.xml @@ -315,6 +315,58 @@ public class Foo { if (getStr("b").equals("ab")) { } // nok if ("ab".equals(getStr("b"))) { } // ok } +} + ]]> + + + + #575 LiteralsFirstInComparisons to consider constant fields, i.e. static final Strings + 1 + + + + + #575 LiteralsFirstInComparisons must not trigger if the field is not final + 0 + + + + + #575 LiteralsFirstInComparisons must not trigger if the field is not static + 0 + + + + + #575 LiteralsFirstInComparisons must not trigger if the constant field is not a String + 0 + diff --git a/pmd-java8/pom.xml b/pmd-java8/pom.xml index 39b093748d..97d9206b84 100644 --- a/pmd-java8/pom.xml +++ b/pmd-java8/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-javascript/pom.xml b/pmd-javascript/pom.xml index c6a0dab926..7cd4b028d3 100644 --- a/pmd-javascript/pom.xml +++ b/pmd-javascript/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-javascript/src/main/ant/alljavacc.xml b/pmd-javascript/src/main/ant/alljavacc.xml index 43c075273e..03be65c6ad 100644 --- a/pmd-javascript/src/main/ant/alljavacc.xml +++ b/pmd-javascript/src/main/ant/alljavacc.xml @@ -29,11 +29,15 @@ - + + + + + + + diff --git a/pmd-javascript/src/test/java/net/sourceforge/pmd/lang/ecmascript/EcmascriptParserOptionsTest.java b/pmd-javascript/src/test/java/net/sourceforge/pmd/lang/ecmascript/EcmascriptParserOptionsTest.java index da18981777..1dce10abb0 100644 --- a/pmd-javascript/src/test/java/net/sourceforge/pmd/lang/ecmascript/EcmascriptParserOptionsTest.java +++ b/pmd-javascript/src/test/java/net/sourceforge/pmd/lang/ecmascript/EcmascriptParserOptionsTest.java @@ -4,7 +4,7 @@ package net.sourceforge.pmd.lang.ecmascript; -import static net.sourceforge.pmd.lang.ParserOptionsTest.verifyOptionsEqualsHashcode; +import static net.sourceforge.pmd.lang.ParserOptionsTestUtils.verifyOptionsEqualsHashcode; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; diff --git a/pmd-jsp/pom.xml b/pmd-jsp/pom.xml index a11e5ad3c0..85e5891550 100644 --- a/pmd-jsp/pom.xml +++ b/pmd-jsp/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-jsp/src/main/ant/alljavacc.xml b/pmd-jsp/src/main/ant/alljavacc.xml index 367e3e14cc..ac06d2af0a 100644 --- a/pmd-jsp/src/main/ant/alljavacc.xml +++ b/pmd-jsp/src/main/ant/alljavacc.xml @@ -28,17 +28,25 @@ - + + + + + - + + + + + + + + + diff --git a/pmd-jsp/src/test/java/net/sourceforge/pmd/lang/jsp/ast/XPathJspRuleTest.java b/pmd-jsp/src/test/java/net/sourceforge/pmd/lang/jsp/ast/XPathJspRuleTest.java index 3597fa8496..9f551f3a11 100644 --- a/pmd-jsp/src/test/java/net/sourceforge/pmd/lang/jsp/ast/XPathJspRuleTest.java +++ b/pmd-jsp/src/test/java/net/sourceforge/pmd/lang/jsp/ast/XPathJspRuleTest.java @@ -18,7 +18,6 @@ import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.RuleSet; import net.sourceforge.pmd.RuleSets; import net.sourceforge.pmd.RuleViolation; -import net.sourceforge.pmd.RulesetsFactoryUtils; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.jsp.JspLanguageModule; import net.sourceforge.pmd.lang.rule.XPathRule; @@ -35,7 +34,7 @@ public class XPathJspRuleTest extends RuleTst { Rule rule = new XPathRule(XPATH_EXPRESSION); rule.setMessage("Test"); rule.setLanguage(LanguageRegistry.getLanguage(JspLanguageModule.NAME)); - RuleSet rules = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(rule); + RuleSet rules = RuleSet.forSingleRule(rule); RuleContext ctx = new RuleContext(); Report report = new Report(); diff --git a/pmd-kotlin/pom.xml b/pmd-kotlin/pom.xml index 0471f85ed6..ad5e204d58 100644 --- a/pmd-kotlin/pom.xml +++ b/pmd-kotlin/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-lang-test/pom.xml b/pmd-lang-test/pom.xml index 1f63a2c9ad..acf724c4f3 100644 --- a/pmd-lang-test/pom.xml +++ b/pmd-lang-test/pom.xml @@ -12,7 +12,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-lua/pom.xml b/pmd-lua/pom.xml index 6618d50ab6..318d4243fe 100644 --- a/pmd-lua/pom.xml +++ b/pmd-lua/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-matlab/pom.xml b/pmd-matlab/pom.xml index 4b510c7b5c..3fadbc21b1 100644 --- a/pmd-matlab/pom.xml +++ b/pmd-matlab/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-matlab/src/main/ant/alljavacc.xml b/pmd-matlab/src/main/ant/alljavacc.xml index a6295481be..b04b340e12 100644 --- a/pmd-matlab/src/main/ant/alljavacc.xml +++ b/pmd-matlab/src/main/ant/alljavacc.xml @@ -29,11 +29,15 @@ - + + + + + + + diff --git a/pmd-modelica/pom.xml b/pmd-modelica/pom.xml index 7e000aec1a..1dad77e3ab 100644 --- a/pmd-modelica/pom.xml +++ b/pmd-modelica/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-modelica/src/main/ant/alljavacc.xml b/pmd-modelica/src/main/ant/alljavacc.xml index 06e0de1824..ae1bb991ed 100644 --- a/pmd-modelica/src/main/ant/alljavacc.xml +++ b/pmd-modelica/src/main/ant/alljavacc.xml @@ -28,15 +28,23 @@ - + + + + + - + + + + + + + diff --git a/pmd-objectivec/pom.xml b/pmd-objectivec/pom.xml index eb05bd86d3..dad8ef3694 100644 --- a/pmd-objectivec/pom.xml +++ b/pmd-objectivec/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-objectivec/src/main/ant/alljavacc.xml b/pmd-objectivec/src/main/ant/alljavacc.xml index 27f2b9e62b..16162ec087 100644 --- a/pmd-objectivec/src/main/ant/alljavacc.xml +++ b/pmd-objectivec/src/main/ant/alljavacc.xml @@ -29,11 +29,15 @@ - + + + + + + + diff --git a/pmd-perl/pom.xml b/pmd-perl/pom.xml index a4b20c2485..5c6f67d9f8 100644 --- a/pmd-perl/pom.xml +++ b/pmd-perl/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-php/pom.xml b/pmd-php/pom.xml index 53370954f8..262882fdf1 100644 --- a/pmd-php/pom.xml +++ b/pmd-php/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-plsql/pom.xml b/pmd-plsql/pom.xml index b11a021f61..302036b5ae 100644 --- a/pmd-plsql/pom.xml +++ b/pmd-plsql/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-plsql/src/main/ant/alljavacc.xml b/pmd-plsql/src/main/ant/alljavacc.xml index d0752af8bf..918858563e 100644 --- a/pmd-plsql/src/main/ant/alljavacc.xml +++ b/pmd-plsql/src/main/ant/alljavacc.xml @@ -28,15 +28,23 @@ - + + + + + - + + + + + + + diff --git a/pmd-python/pom.xml b/pmd-python/pom.xml index 148cb89aaf..7d0242adbf 100644 --- a/pmd-python/pom.xml +++ b/pmd-python/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-python/src/main/ant/alljavacc.xml b/pmd-python/src/main/ant/alljavacc.xml index e62797eed7..2e3e8c3cbe 100644 --- a/pmd-python/src/main/ant/alljavacc.xml +++ b/pmd-python/src/main/ant/alljavacc.xml @@ -29,11 +29,15 @@ - + + + + + + + diff --git a/pmd-ruby/pom.xml b/pmd-ruby/pom.xml index 7e3bd5faff..5b7eff715d 100644 --- a/pmd-ruby/pom.xml +++ b/pmd-ruby/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-scala-modules/pmd-scala-common/pom.xml b/pmd-scala-modules/pmd-scala-common/pom.xml index eba47ba601..c64d9a09da 100644 --- a/pmd-scala-modules/pmd-scala-common/pom.xml +++ b/pmd-scala-modules/pmd-scala-common/pom.xml @@ -8,7 +8,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../.. 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 1f37f484c7..984277dd0a 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 @@ -13,7 +13,6 @@ import net.sourceforge.pmd.Report; 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.ast.test.BaseParsingHelper; import net.sourceforge.pmd.lang.scala.ScalaLanguageModule; @@ -38,7 +37,7 @@ public final class ScalaParsingHelper extends BaseParsingHelper net.sourceforge.pmd pmd-scala-common - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../pmd-scala-common diff --git a/pmd-scala-modules/pmd-scala_2.13/pom.xml b/pmd-scala-modules/pmd-scala_2.13/pom.xml index 1f256859a7..192cd100fa 100644 --- a/pmd-scala-modules/pmd-scala_2.13/pom.xml +++ b/pmd-scala-modules/pmd-scala_2.13/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd-scala-common - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../pmd-scala-common diff --git a/pmd-scala/pom.xml b/pmd-scala/pom.xml index 97c49136d2..c9a1ed0d6b 100644 --- a/pmd-scala/pom.xml +++ b/pmd-scala/pom.xml @@ -9,7 +9,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-swift/pom.xml b/pmd-swift/pom.xml index 4e3ac90b76..3aebee81e7 100644 --- a/pmd-swift/pom.xml +++ b/pmd-swift/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-swift/src/main/antlr4/net/sourceforge/pmd/lang/swift/antlr4/Swift.g4 b/pmd-swift/src/main/antlr4/net/sourceforge/pmd/lang/swift/antlr4/Swift.g4 index 01ee4ad84c..2c44bc62f2 100644 --- a/pmd-swift/src/main/antlr4/net/sourceforge/pmd/lang/swift/antlr4/Swift.g4 +++ b/pmd-swift/src/main/antlr4/net/sourceforge/pmd/lang/swift/antlr4/Swift.g4 @@ -1018,7 +1018,7 @@ ImplicitParameterName : '$' DecimalLiteral ; // TODO: don't allow '_' here // GRAMMAR OF A LITERAL booleanLiteral: BooleanLiteral ; -literal : numericLiteral | MultiStringLiteral | SingleStringLiteral | BooleanLiteral | NilLiteral ; +literal : numericLiteral | MultiStringLiteral | SingleStringLiteral | BooleanLiteral | NilLiteral | RawMultiStringLiteral | RawSingleStringLiteral ; // GRAMMAR OF AN INTEGER LITERAL @@ -1073,11 +1073,16 @@ TRIPLEDQUOTES : '"""' ; MultiStringLiteral : TRIPLEDQUOTES '\n' .*? '\n' TRIPLEDQUOTES ; fragment MultiQuotedText : MultiQuotedTextItem+ ; -fragment MultiQuotedTextItem : MultiInterpolatedString - | ~[\\\u000A\u000D] - ; +fragment MultiQuotedTextItem : MultiInterpolatedString | ~[\\\u000A\u000D] ; fragment MultiInterpolatedString: '\\(' (MultiQuotedTextItem | SingleStringLiteral)* ')'; +// swift 5 extended delimiter, eg ##"abc"## +RawSingleStringLiteral : '#"' RawSingleQuotedTextItem* '"#' | '#' RawSingleStringLiteral '#'; +fragment RawSingleQuotedTextItem : ~[\u000A\u000D] ; + +RawMultiStringLiteral : '#"""' RawMultiQuotedTextItem* '"""#' | '#' RawMultiStringLiteral '#'; +fragment RawMultiQuotedTextItem : . ; + // StringLiteral : '"' QuotedText? '"' ; SingleStringLiteral : '"' QuotedText? '"' ; fragment SingleDoubleQuote : '"' | ~["] ; diff --git a/pmd-swift/src/test/resources/net/sourceforge/pmd/lang/swift/cpd/testdata/Swift5.0.swift b/pmd-swift/src/test/resources/net/sourceforge/pmd/lang/swift/cpd/testdata/Swift5.0.swift index 6e77f1dd29..85bd015309 100644 --- a/pmd-swift/src/test/resources/net/sourceforge/pmd/lang/swift/cpd/testdata/Swift5.0.swift +++ b/pmd-swift/src/test/resources/net/sourceforge/pmd/lang/swift/cpd/testdata/Swift5.0.swift @@ -38,3 +38,16 @@ var x = 2 print(x[keyPath: id]) // prints 2 x[keyPath: id] = 3 print(x[keyPath: id]) // prints 3 + +// https://www.swiftbysundell.com/articles/string-literals-in-swift/ +let rawString = #"Press "Continue" to close this dialog."# +extension URL { + func html(withTitle title: String) -> String { + return ##"\#(title)"## + } +} + +let rawMultiString = ###"a\###"### +let rawMultiString2 = ###"""a\### +""hey"" +"""### diff --git a/pmd-swift/src/test/resources/net/sourceforge/pmd/lang/swift/cpd/testdata/Swift5.0.txt b/pmd-swift/src/test/resources/net/sourceforge/pmd/lang/swift/cpd/testdata/Swift5.0.txt index 5eaa13610c..cb66d0c607 100644 --- a/pmd-swift/src/test/resources/net/sourceforge/pmd/lang/swift/cpd/testdata/Swift5.0.txt +++ b/pmd-swift/src/test/resources/net/sourceforge/pmd/lang/swift/cpd/testdata/Swift5.0.txt @@ -197,4 +197,42 @@ L40 [id] 18 19 [\]] 20 20 [)] 21 21 +L43 + [let] 1 3 + [rawString] 5 13 + [=] 15 15 + [#"Press "Continue" to close this d[ 17 58 +L44 + [extension] 1 9 + [URL] 11 13 + [{] 15 15 +L45 + [func] 5 8 + [html] 10 13 + [(] 14 14 + [withTitle] 15 23 + [title] 25 29 + [:] 30 30 + [String] 32 37 + [)] 38 38 + [->] 40 41 + [String] 43 48 + [{] 50 50 +L46 + [return] 9 14 + [##" net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ 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 bfa7f0a765..814a349387 100644 --- a/pmd-test/src/main/java/net/sourceforge/pmd/AbstractRuleSetFactoryTest.java +++ b/pmd-test/src/main/java/net/sourceforge/pmd/AbstractRuleSetFactoryTest.java @@ -17,6 +17,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -59,8 +60,20 @@ public abstract class AbstractRuleSetFactoryTest { 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()); } @@ -263,6 +276,9 @@ public abstract class AbstractRuleSetFactoryTest { List result = new ArrayList<>(); for (Language language : LanguageRegistry.getLanguages()) { + if (this.languagesToSkip.contains(language.getTerseName())) { + continue; + } result.addAll(getRuleSetFileNames(language.getTerseName())); } 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 945946df05..f5127614e1 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 { /** @@ -53,42 +55,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..cab491c5e8 --- /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.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()); + } +} 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 2b0d6ef46a..1207929f00 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 @@ -294,7 +294,7 @@ public abstract class RuleTst { ctx.setSourceCodeFile(new File("n/a")); ctx.setLanguageVersion(languageVersion); ctx.setIgnoreExceptions(false); - RuleSet rules = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(rule); + RuleSet rules = RuleSet.forSingleRule(rule); p.getSourceCodeProcessor().processSourceCode(new StringReader(code), new RuleSets(rules), ctx); } catch (Exception e) { throw new RuntimeException(e); 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..0fc7a8aa10 --- /dev/null +++ b/pmd-test/src/test/java/net/sourceforge/pmd/lang/ParserOptionsUnitTest.java @@ -0,0 +1,205 @@ +/** + * 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.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.assertNull(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 4030771198..94448ea109 100644 --- a/pmd-visualforce/pom.xml +++ b/pmd-visualforce/pom.xml @@ -7,10 +7,14 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ + + 8 + + @@ -77,6 +81,12 @@ pmd-core + + net.sourceforge.pmd + pmd-apex + ${project.version} + + junit junit diff --git a/pmd-visualforce/src/main/ant/alljavacc.xml b/pmd-visualforce/src/main/ant/alljavacc.xml index b8b6b922c9..decd115135 100644 --- a/pmd-visualforce/src/main/ant/alljavacc.xml +++ b/pmd-visualforce/src/main/ant/alljavacc.xml @@ -28,18 +28,27 @@ - - - + + + + + + + + + + + + + + + + + diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ApexClassPropertyTypes.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ApexClassPropertyTypes.java new file mode 100644 index 0000000000..2de3e5c277 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ApexClassPropertyTypes.java @@ -0,0 +1,98 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf; + +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 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/ApexClassPropertyTypesVisitor.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ApexClassPropertyTypesVisitor.java new file mode 100644 index 0000000000..79a109cc48 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/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; + +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/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/ObjectFieldTypes.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ObjectFieldTypes.java new file mode 100644 index 0000000000..8d32f45d84 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ObjectFieldTypes.java @@ -0,0 +1,253 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf; + +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; + +/** + * 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/SalesforceFieldTypes.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/SalesforceFieldTypes.java new file mode 100644 index 0000000000..442ad5a983 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/SalesforceFieldTypes.java @@ -0,0 +1,99 @@ +/** + * 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; +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; + +/** + * 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/VfExpressionTypeVisitor.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfExpressionTypeVisitor.java new file mode 100644 index 0000000000..8b4db528cd --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfExpressionTypeVisitor.java @@ -0,0 +1,182 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf; + +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.ast.ASTAttribute; +import net.sourceforge.pmd.lang.vf.ast.ASTAttributeValue; +import net.sourceforge.pmd.lang.vf.ast.ASTCompilationUnit; +import net.sourceforge.pmd.lang.vf.ast.ASTElExpression; +import net.sourceforge.pmd.lang.vf.ast.ASTElement; +import net.sourceforge.pmd.lang.vf.ast.ASTExpression; +import net.sourceforge.pmd.lang.vf.ast.ASTText; +import net.sourceforge.pmd.lang.vf.ast.VfAstInternals; +import net.sourceforge.pmd.lang.vf.ast.VfParserVisitorAdapter; +import net.sourceforge.pmd.lang.vf.ast.VfTypedNode; + +/** + * 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/VfHandler.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfHandler.java index e40d90f8ab..31fd287511 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 @@ -28,6 +28,11 @@ public class VfHandler extends AbstractLanguageVersionHandler { return new VfParser(parserOptions); } + @Override + public ParserOptions getDefaultParserOptions() { + return new VfParserOptions(); + } + @Deprecated @Override public VisitorStarter getDumpFacade(final Writer writer, final String prefix, final boolean recurse) { diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfParser.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfParser.java index e40938a291..0fec1232cb 100644 --- a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfParser.java +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/VfParser.java @@ -17,6 +17,7 @@ import net.sourceforge.pmd.lang.ast.AbstractTokenManager; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.ast.ParseException; import net.sourceforge.pmd.lang.ast.SimpleCharStream; +import net.sourceforge.pmd.lang.vf.ast.ASTCompilationUnit; /** * Adapter for the VfParser. @@ -26,7 +27,6 @@ import net.sourceforge.pmd.lang.ast.SimpleCharStream; @Deprecated @InternalApi public class VfParser extends AbstractParser { - public VfParser(ParserOptions parserOptions) { super(parserOptions); } @@ -44,7 +44,13 @@ public class VfParser extends AbstractParser { @Override public Node parse(String fileName, Reader source) throws ParseException { AbstractTokenManager.setFileName(fileName); - return new net.sourceforge.pmd.lang.vf.ast.VfParser(new SimpleCharStream(source)).CompilationUnit(); + ASTCompilationUnit astCompilationUnit = new net.sourceforge.pmd.lang.vf.ast.VfParser( + new SimpleCharStream(source)).CompilationUnit(); + // Add type information to the AST + VfExpressionTypeVisitor visitor = new VfExpressionTypeVisitor(fileName, (VfParserOptions) this.getParserOptions()); + visitor.visit(astCompilationUnit, null); + + return astCompilationUnit; } @Override 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 a84eb74fb0..01cfb488a3 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,9 +4,27 @@ 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.annotation.InternalApi; +import net.sourceforge.pmd.lang.ast.AbstractTokenManager; +import net.sourceforge.pmd.lang.ast.Node; public class ASTExpression extends AbstractVFNode { + private static final Logger LOGGER = Logger.getLogger(ASTExpression.class.getName()); + + /** + * 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 { + } + @Deprecated @InternalApi public ASTExpression(int id) { @@ -23,4 +41,120 @@ public class ASTExpression extends AbstractVFNode { public Object jjtAccept(VfParserVisitor visitor, Object 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; + } } 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 fa7d70601c..717651b14f 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 @@ -6,7 +6,8 @@ package net.sourceforge.pmd.lang.vf.ast; import net.sourceforge.pmd.annotation.InternalApi; -public class ASTIdentifier extends AbstractVFNode { +public class ASTIdentifier extends AbstractVFDataNode { + @Deprecated @InternalApi public ASTIdentifier(int 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 74450f65b8..f2516b54c6 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 @@ -6,7 +6,7 @@ package net.sourceforge.pmd.lang.vf.ast; import net.sourceforge.pmd.annotation.InternalApi; -public class ASTLiteral extends AbstractVFNode { +public class ASTLiteral extends AbstractVFDataNode { @Deprecated @InternalApi public ASTLiteral(int 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..88c65928b6 --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/ast/AbstractVFDataNode.java @@ -0,0 +1,33 @@ +/* + * 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. + */ +class AbstractVFDataNode extends AbstractVFNode implements VfTypedNode { + + private DataType dataType; + + AbstractVFDataNode(int id) { + super(id); + } + + AbstractVFDataNode(VfParser parser, int id) { + super(id); + this.parser = parser; + } + + @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/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/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 51817b896c..77527770e5 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.AbstractVFNode; +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 AbstractVFNode expression) { final ASTExpression ex = expression.getFirstChildOfType(ASTExpression.class); diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ApexClassPropertyTypesTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ApexClassPropertyTypesTest.java new file mode 100644 index 0000000000..866f37c213 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ApexClassPropertyTypesTest.java @@ -0,0 +1,70 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf; + +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; + +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(); + + VFTestUtils.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/ApexClassPropertyTypesVisitorTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ApexClassPropertyTypesVisitorTest.java new file mode 100644 index 0000000000..ef74216593 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ApexClassPropertyTypesVisitorTest.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 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 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/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/ObjectFieldTypesTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ObjectFieldTypesTest.java new file mode 100644 index 0000000000..398d892eb6 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ObjectFieldTypesTest.java @@ -0,0 +1,113 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf; + +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; + +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) { + VFTestUtils.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) { + VFTestUtils.validateDataTypes(EXPECTED_MDAPI_DATA_TYPES, objectFieldTypes, vfPagePath, paths); + } +} 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..e403ecd178 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/VFTestUtils.java @@ -0,0 +1,107 @@ +/** + * 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 java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import net.sourceforge.pmd.RuleContext; +import net.sourceforge.pmd.lang.ast.Node; +import net.sourceforge.pmd.lang.rule.xpath.SaxonXPathRuleQuery; +import net.sourceforge.pmd.lang.rule.xpath.XPathRuleQuery; +import net.sourceforge.pmd.properties.PropertyDescriptor; + +public final class 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(); + } + + /** + * @return all nodes that match the {@code xpath} version 2 query. + */ + public static List findNodes(Node node, String xpath) { + SaxonXPathRuleQuery query = createQuery(xpath); + return query.evaluate(node, new RuleContext()); + } + + /** + * 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)); + } + } + + private static SaxonXPathRuleQuery createQuery(String xpath) { + SaxonXPathRuleQuery query = new SaxonXPathRuleQuery(); + query.setVersion(XPathRuleQuery.XPATH_2_0); + query.setProperties(Collections., Object>emptyMap()); + query.setXPath(xpath); + return query; + } + + private VFTestUtils() { + } +} diff --git a/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/VfExpressionTypeVisitorTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/VfExpressionTypeVisitorTest.java new file mode 100644 index 0000000000..f670dba54b --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/VfExpressionTypeVisitorTest.java @@ -0,0 +1,187 @@ +/** + * 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.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 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.vf.ast.ASTIdentifier; +import net.sourceforge.pmd.lang.vf.ast.ASTLiteral; +import net.sourceforge.pmd.lang.vf.ast.VfTypedNode; +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()) { + String xpath = String.format("//Identifier[@Image='%s' and @DataType='%s']", entry.getKey(), entry.getValue().name()); + List nodes = VFTestUtils.findNodes(rootNode, xpath); + // 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()) { + // Literals are surrounded by apostrophes + String xpath = String.format("//Literal[@Image=\"'%s'\" and @DataType='']", entry.getKey()); + List nodes = VFTestUtils.findNodes(rootNode, xpath); + // 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"); + + for (String xpath : new String[] { "//Identifier[@Image='NotFoundField__c']", "//Literal[@Image=\"'NotFoundField__c'\"]" }) { + List nodes = VFTestUtils.findNodes(rootNode, xpath); + // Each string appears twice, it is set on a "value" attribute and inline + assertEquals(2, nodes.size()); + for (Node node : nodes) { + assertTrue(node.getClass().getSimpleName(), node instanceof VfTypedNode); + VfTypedNode dataNode = (VfTypedNode) node; + assertNull(dataNode.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()) { + String xpath = String.format("//Identifier[@Image='%s' and @DataType='%s']", entry.getKey(), entry.getValue().name()); + List nodes = VFTestUtils.findNodes(rootNode, xpath); + // 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()); + } + } + } + + /** + * Nodes where the DataType can't be determined should have a null DataType + */ + @Test + public void testDataTypeForApexPropertiesNotFound() throws FileNotFoundException { + Node rootNode = compile("ApexController.page"); + + String xpath = "//Identifier[@Image='NotFoundProp']"; + List nodes = VFTestUtils.findNodes(rootNode, xpath); + // Each string appears twice, it is set on a "value" attribute and inline + assertEquals(2, nodes.size()); + for (Node node : nodes) { + assertTrue(node.getClass().getSimpleName(), node instanceof VfTypedNode); + VfTypedNode dataNode = (VfTypedNode) node; + assertNull(dataNode.getDataType()); + } + } + + 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/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..135c46f443 --- /dev/null +++ b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/ast/ASTExpressionTest.java @@ -0,0 +1,237 @@ +/** + * 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.io.StringReader; +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.lang.ast.SimpleCharStream; +import net.sourceforge.pmd.lang.vf.VFTestUtils; +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 = VFTestUtils.findNodes(compilationUnit, "//Expression"); + 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 = VFTestUtils.findNodes(compilationUnit, "//Expression"); + 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 = VFTestUtils.findNodes(compilationUnit, "//Expression"); + 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 = VFTestUtils.findNodes(compilationUnit, "//Expression"); + 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 = VFTestUtils.findNodes(compilationUnit, "//Expression"); + 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 = VFTestUtils.findNodes(compilationUnit, "//Expression"); + 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 = VFTestUtils.findNodes(compilationUnit, "//Expression"); + 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 = VFTestUtils.findNodes(compilationUnit, "//Expression"); + 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 = VFTestUtils.findNodes(compilationUnit, "//Expression"); + 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 + } + } + } + + /** + * 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 = new net.sourceforge.pmd.lang.vf.ast.VfParser( + new SimpleCharStream(new StringReader("" + + snippet + + ""))).CompilationUnit(); + + 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/rule/security/VfUnescapeElTest.java b/pmd-visualforce/src/test/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElTest.java index 40212b810e..5a407676a8 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,161 @@ 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.RulesetsFactoryUtils; +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 = RulesetsFactoryUtils.defaultFactory().createSingleRuleRuleSet(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/ApexClassPropertyTypes/metadata/sfdx/classes/ApexController.cls b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/ApexClassPropertyTypes/metadata/sfdx/pages/SomePage.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/ApexClassPropertyTypesVisitor/metadata/sfdx/classes/ApexController.cls b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/ObjectFieldTypes/metadata/mdapi/objects/Account.object b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/ObjectFieldTypes/metadata/mdapi/pages/SomePage.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/Checkbox__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/DateTime__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/LongTextArea__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/Picklist__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/TextArea__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/ObjectFieldTypes/metadata/sfdx/objects/Account/fields/Text__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/ObjectFieldTypes/metadata/sfdx/pages/SomePage.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/VfExpressionTypeVisitor/metadata/sfdx/classes/ApexController.cls b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/Checkbox__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/DateTime__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/LongTextArea__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/Picklist__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/TextArea__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/VfExpressionTypeVisitor/metadata/sfdx/objects/Account/fields/Text__c.field-meta.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/VfExpressionTypeVisitor/metadata/sfdx/pages/ApexController.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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/VfExpressionTypeVisitor/metadata/sfdx/pages/StandardAccount.page b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/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/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-vm/pom.xml b/pmd-vm/pom.xml index 89b9c6d6f1..65a85d82d5 100644 --- a/pmd-vm/pom.xml +++ b/pmd-vm/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ diff --git a/pmd-vm/src/main/ant/alljavacc.xml b/pmd-vm/src/main/ant/alljavacc.xml index 4650be5669..8cdf20f877 100644 --- a/pmd-vm/src/main/ant/alljavacc.xml +++ b/pmd-vm/src/main/ant/alljavacc.xml @@ -28,17 +28,25 @@ - + + + + + - + + + + + + + + + diff --git a/pmd-xml/pom.xml b/pmd-xml/pom.xml index 3392c50697..40ce8c631b 100644 --- a/pmd-xml/pom.xml +++ b/pmd-xml/pom.xml @@ -7,7 +7,7 @@ net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT ../ 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 f9c9f2529c..b7bec79b7b 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 org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; diff --git a/pom.xml b/pom.xml index 6cc41bf1d9..d7c11a0bda 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 net.sourceforge.pmd pmd - 6.30.0-SNAPSHOT + 6.31.0-SNAPSHOT pom PMD @@ -76,7 +76,7 @@ - 2020-10-24T08:17:24Z + 2020-12-12T08:42:10Z 7 @@ -724,7 +724,7 @@ org.codehaus.groovy groovy - 2.4.7 + 2.4.21 @@ -1082,7 +1082,6 @@ pmd-ruby pmd-swift pmd-test - pmd-visualforce pmd-vm pmd-xml @@ -1096,5 +1095,6 @@ pmd-scala-modules/pmd-scala-common pmd-scala-modules/pmd-scala_2.13 pmd-scala-modules/pmd-scala_2.12 + pmd-visualforce