Merge branch 'pmd/7.0.x' into java-grammar
This commit is contained in:
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
labels: 'a:bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
labels: 'an:enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
2
.github/ISSUE_TEMPLATE/new_rule.md
vendored
2
.github/ISSUE_TEMPLATE/new_rule.md
vendored
@ -2,7 +2,7 @@
|
||||
name: New Rule
|
||||
about: You have an idea for a new rule? Great!
|
||||
title: ''
|
||||
labels: new-rule
|
||||
labels: 'a:new-rule'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
2
.github/ISSUE_TEMPLATE/question.md
vendored
2
.github/ISSUE_TEMPLATE/question.md
vendored
@ -2,7 +2,7 @@
|
||||
name: Question
|
||||
about: Feel free to ask any question about PMD and its usage
|
||||
title: ''
|
||||
labels: question
|
||||
labels: 'a:question'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
2
.github/ISSUE_TEMPLATE/rule_violation.md
vendored
2
.github/ISSUE_TEMPLATE/rule_violation.md
vendored
@ -2,7 +2,7 @@
|
||||
name: Rule violation
|
||||
about: Let us know about a false positive/false negative
|
||||
title: ''
|
||||
labels: bug
|
||||
labels: 'a:bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
@ -445,4 +445,7 @@ entries:
|
||||
- title: Merging pull requests
|
||||
url: /pmd_projectdocs_committers_merging_pull_requests.html
|
||||
output: web, pdf
|
||||
- title: Main Landing page
|
||||
url: /pmd_projectdocs_committers_main_landing_page.html
|
||||
output: web, pdf
|
||||
|
||||
|
85
docs/pages/pmd/projectdocs/committers/main_landing_page.md
Normal file
85
docs/pages/pmd/projectdocs/committers/main_landing_page.md
Normal file
@ -0,0 +1,85 @@
|
||||
---
|
||||
title: Main Landing Page
|
||||
permalink: pmd_projectdocs_committers_main_landing_page.html
|
||||
last_updated: March 2020
|
||||
author: Andreas Dangel <andreas.dangel@pmd-code.org>
|
||||
---
|
||||
|
||||
The main homepage of PMD <https://pmd.github.io> is hosted by Github Pages.
|
||||
|
||||
The repository is <https://github.com/pmd/pmd.github.io>.
|
||||
|
||||
It uses [Jekyll](https://jekyllrb.com/) to generate the static html pages. Jekyll is
|
||||
executed by github for every push to the repository. Please note, that it takes some time
|
||||
until Jekyll has been executed and due to caching, the homepage is not updated immediately.
|
||||
It usually takes 15 minutes.
|
||||
|
||||
|
||||
## Contents
|
||||
|
||||
* Main page - aka "Landing page": <https://pmd.github.io>
|
||||
* Layout: [_layouts/default.html](https://github.com/pmd/pmd.github.io/blob/master/_layouts/default.html).
|
||||
It includes all the sub section, which can be found in the includes directory [_includes/](https://github.com/pmd/pmd.github.io/tree/master/_includes)
|
||||
* The latest PMD version is configured in `_config.yml` and the variables `site.pmd.latestVersion` are used
|
||||
e.g. in [_includes/home.html](https://github.com/pmd/pmd.github.io/blob/master/_includes/home.html).
|
||||
* Blog - aka "News": <https://pmd.github.io/news/>
|
||||
* This is a section on main page. It shows the 5 latest news. See [_includes/news.html](https://github.com/pmd/pmd.github.io/blob/master/_includes/news.html).
|
||||
* There is also a sub page "news" which lists all news.
|
||||
* Layout: [_layouts/news.html](https://github.com/pmd/pmd.github.io/blob/master/_layouts/news.html)
|
||||
* Page (which is pretty empty): [news.html](https://github.com/pmd/pmd.github.io/blob/master/news.html)
|
||||
* Documentation for the latest release: <https://pmd.github.io/latest/>
|
||||
* The PMD documentation of the latest release is simply copied as static html into the folder [latest/](https://github.com/pmd/pmd.github.io/tree/master/latest).
|
||||
This makes the latest release documentation available under the stable URL
|
||||
<https://pmd.github.io/latest/>. This URL is also used for the [sitemap.xml](https://github.com/pmd/pmd.github.io/blob/master/sitemap.xml).
|
||||
* Documentation for previous releases are still being kept under the folders `pmd-<version>/`.
|
||||
|
||||
|
||||
## Building the page locally
|
||||
|
||||
Since the repository contains the documentation for many old PMD releases, it is quite big. When executing
|
||||
Jekyll to generate the site, it copies all the files to the folder `_site/` - and this can take a while.
|
||||
|
||||
In order to speed things up locally, consider to add `pmd-*` to the exclude patterns in `_config.yml`. See
|
||||
also the comments in this file.
|
||||
|
||||
Then it is a matter of simply executing `bundle exec jekyll serve`. This will generate the site and host
|
||||
it on localhost, so you can test the page at <http://127.0.0.1:4000>.
|
||||
|
||||
|
||||
## Updates during a release
|
||||
|
||||
When creating a new PMD release, some content of the main page need to be updated as well.
|
||||
This done as part of the [Release process](pmd_projectdocs_committers_releasing.html), but is
|
||||
summarized here as well:
|
||||
|
||||
* The versions (e.g. `pmd.latestVersion`) needs to be updated in `_config.yml`
|
||||
* This is needed to generate the correct links and texts for the latest version on landing page
|
||||
* The new PMD documentation needs to be copied to `/pmd-<version>/`
|
||||
* Then this folder needs to copied to `/latest/`, actually replacing the old version.
|
||||
* A new blog post with release notes is added: `/_posts/YYYY-mm-dd-PMD-<version>.md`
|
||||
* The sitemap `sitemap.xml` is regenerated
|
||||
|
||||
Some of these steps are automated through `do-release.sh` (like blog post), some are manual steps
|
||||
(updating the version in _config.yml) and other steps are done on the travis-ci-build (like
|
||||
copying the new documentation).
|
||||
|
||||
## Adding a new blog post
|
||||
|
||||
Adding a new blog post is as easy as:
|
||||
|
||||
* Creating a new file in the folder "_posts": `/_posts/YYYY-mm-dd-<title>.md`
|
||||
* The file name needs to fit this pattern. The date of the blog post is taken from the file name. The "<title>"
|
||||
is used for the url.
|
||||
* The file is a markdown file starting with a frontmatter for jekyll. Just use this template for the new file:
|
||||
|
||||
```
|
||||
---
|
||||
layout: post
|
||||
title: Title
|
||||
---
|
||||
|
||||
Here comes the text
|
||||
```
|
||||
|
||||
Once you commit and push it, Github will run Jekyll and update the page. The Jekyll templates take care that
|
||||
the new post is recognized and added to the news section and also on the news subpage.
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Releasing
|
||||
title: Release process
|
||||
permalink: pmd_projectdocs_committers_releasing.html
|
||||
author: Romain Pelisse <rpelisse@users.sourceforge.net>, Andreas Dangel <adangel@users.sourceforge.net>
|
||||
---
|
||||
|
@ -24,7 +24,7 @@ untouched, files with violations will be listed with full detail. Therefore, its
|
||||
Incremental analysis is enabled automatically once a location to store the cache has been defined.
|
||||
From command-line that is done through the [`-cache`](pmd_userdocs_cli_reference.html#cache) argument, but support for the feature is
|
||||
available for tools integrating PMD such as [Ant](pmd_userdocs_tools_ant.html),
|
||||
[Maven](pmd_userdocs_tools_maven.html), and Gradle.
|
||||
[Maven](pmd_userdocs_tools_maven.html), and [Gradle](pmd_userdocs_tools_gradle.html).
|
||||
|
||||
|
||||
### Disabling incremental analysis
|
||||
@ -32,3 +32,83 @@ available for tools integrating PMD such as [Ant](pmd_userdocs_tools_ant.html),
|
||||
By default, PMD will suggest to use an analysis cache by logging a warning.
|
||||
If you'd like to disable this warning, or ignore the analysis cache for a
|
||||
few runs, you can use the [`-no-cache`](pmd_userdocs_cli_reference.html#no-cache) switch.
|
||||
|
||||
|
||||
### FAQ
|
||||
|
||||
#### When is the cache invalidated?
|
||||
|
||||
On the following reasons, the complete cache file is considered invalid:
|
||||
|
||||
* The PMD version differs. Since each PMD version might have fixed some false-positives or false-negatives for rules,
|
||||
a cache file created with a different version is considered invalid. The version comparison is exact.
|
||||
* The used ruleset has been changed. If the ruleset is changed in any way (e.g. adding/removing rules, changing
|
||||
rule properties, ...), the cache is considered invalid.
|
||||
* The [`auxclasspath`](pmd_userdocs_cli_reference.html#auxclasspath) changed. The auxclasspath is used during
|
||||
type resolution. A changed auxclasspath can result for rules, that use type resolution, in different
|
||||
violations. Usually, if the auxclasspath is correct and type resolution works, the rules report less false-positives.
|
||||
To make sure, the correct violations are reported, the cache is considered invalid, if the auxclasspath has changed.
|
||||
* The execution classpath has been changed. On the execution classpath not only the PMD classes are located, but also
|
||||
the implementation of e.g. custom rules. If any jar file/class file on the execution classpath is changed, then
|
||||
the cache is considered invalid as well.
|
||||
|
||||
#### What is stored in the cache file?
|
||||
|
||||
The cache file consists of a header and a body. The header stores the information which is used to decided
|
||||
whether the whole cache file is valid or not (see above). The following information is stored:
|
||||
|
||||
* PMD Version
|
||||
* Ruleset checksum
|
||||
* Auxclasspath checksum
|
||||
* Execution classpath checksum
|
||||
|
||||
The body contains an entry for every file that has been analyzed. For every file, the following information
|
||||
is stored:
|
||||
|
||||
* The full (absolute) pathname of the file
|
||||
* The checksum of the file itself
|
||||
* 0 or more rule violations with all the info (line number, etc.)
|
||||
|
||||
You can think of the cache as a Map where the filepath is used as the key
|
||||
and the violations found in previous runs are the value.
|
||||
|
||||
The cache is in the end just a file with serialized data (binary). The implementation is
|
||||
{% jdoc core::cache.FileAnalysisCache %}.
|
||||
|
||||
#### How does PMD detect whether a file has been changed?
|
||||
|
||||
When analyzing a file, PMD records the checksum of the file content and stores this
|
||||
together with the violations in the cache file. When running PMD with the cache file,
|
||||
PMD looks up the file in the cache and compares the checksums.
|
||||
If the checksums match, then the file is not even parsed, the rules
|
||||
are not executed and the violations for this file are entirely used from the cache.
|
||||
If the checksum doesn't match, then the cached violations are discarded (if there are any)
|
||||
and the file is fully processed: the file is parsed and all the rules are run for it.
|
||||
After we are done, the cache is updated with the new violations.
|
||||
|
||||
#### Can I reuse a cache created on branch A for analyzing my project on branch B?
|
||||
|
||||
This is possible. As long as the same PMD version and same ruleset is used on both branches.
|
||||
Also note, that if the branch uses a different dependencies, the auxclasspath is different on both
|
||||
classes, which invalidates the cache completely. If you project uses e.g. Maven for dependency
|
||||
management and your branch uses different dependencies (either different version or completely different
|
||||
artifacts), then the auxclasspath is changed.
|
||||
|
||||
If files have been renamed on the branch, these files will be analyzed again since PMD uses
|
||||
the file names to assign existing rule violations from the cache. Also, if the full path name
|
||||
of the file changes, because the other branch is checked out at a different location, then all
|
||||
the cached files don't match.
|
||||
|
||||
Apart from these restrictions, PMD will only analyze files that changed between runs.
|
||||
If your previous run was on branch A and then you run on branch B using the same cache file,
|
||||
it will only look at files that are different between the 2 branches.
|
||||
|
||||
#### Can I reuse a cache file across different machines?
|
||||
|
||||
This is only possible, if the other machine uses the exact same path names. That means that
|
||||
your project needs to be checked out into the same directory structure.
|
||||
|
||||
Additionally, all the other restrictions apply (same PMD version, same ruleset, same auxclasspath,
|
||||
same execution classpath).
|
||||
|
||||
See also issue [#2063 [core] Support sharing incremental analysis cache file across different machines](https://github.com/pmd/pmd/issues/2063).
|
||||
|
@ -35,13 +35,30 @@ not change the result of your rules*, if it does, please report a bug at https:/
|
||||
Note that XPath 1.0 support, the default XPath version, is deprecated since PMD 6.22.0.
|
||||
**We highly recommend that you upgrade your rules to XPath 2.0**. Please refer to the [migration guide](https://pmd.github.io/latest/pmd_userdocs_extending_writing_xpath_rules.html#migrating-from-10-to-20).
|
||||
|
||||
#### New Rules
|
||||
|
||||
* The new Apex rule {% rule "apex/codestyle/FieldDeclarationsShouldBeAtStart" %} (`apex-codestyle`)
|
||||
helps to ensure that field declarations are always at the beginning of a class.
|
||||
|
||||
* The new Apex rule {% rule "apex/bestpractices/UnusedLocalVariable" %} (`apex-bestpractices`) detects unused
|
||||
local variables.
|
||||
|
||||
### Fixed Issues
|
||||
|
||||
* apex
|
||||
* [#2210](https://github.com/pmd/pmd/issues/2210): \[apex] ApexCRUDViolation: Support WITH SECURITY_ENFORCED
|
||||
* apex-design
|
||||
* [#2358](https://github.com/pmd/pmd/issues/2358): \[apex] Invalid Apex in Cognitive Complexity tests
|
||||
* apex-security
|
||||
* [#2210](https://github.com/pmd/pmd/issues/2210): \[apex] ApexCRUDViolation: Support WITH SECURITY_ENFORCED
|
||||
* [#2399](https://github.com/pmd/pmd/issues/2399): \[apex] ApexCRUDViolation: false positive with security enforced with line break
|
||||
* core
|
||||
* [#2355](https://github.com/pmd/pmd/issues/2355): \[doc] Improve documentation about incremental analysis
|
||||
* [#2356](https://github.com/pmd/pmd/issues/2356): \[doc] Add missing doc about pmd.github.io
|
||||
* java
|
||||
* [#2378](https://github.com/pmd/pmd/issues/2378): \[java] AbstractJUnitRule has bad performance on large code bases
|
||||
* java-codestyle
|
||||
* [#1723](https://github.com/pmd/pmd/issues/1723): \[java] UseDiamondOperator false-positive inside lambda
|
||||
* java-design
|
||||
* [#2390](https://github.com/pmd/pmd/issues/2390): \[java] AbstractClassWithoutAnyMethod: missing violation for nested classes
|
||||
|
||||
### API Changes
|
||||
|
||||
@ -101,6 +118,12 @@ implementations, and their corresponding Parser if it exists (in the same packag
|
||||
* {% jdoc matlab::lang.matlab.MatlabTokenManager %}
|
||||
* {% jdoc objectivec::lang.objectivec.ObjectiveCTokenManager %}
|
||||
|
||||
##### For removal
|
||||
|
||||
* {% jdoc !!core::lang.Parser#getTokenManager(java.lang.String,java.io.Reader) %}
|
||||
* {% jdoc !!core::lang.TokenManager#setFileName(java.lang.String) %}
|
||||
* {% jdoc !!core::lang.ast.AbstractTokenManager#setFileName(java.lang.String) %}
|
||||
* {% jdoc !!core::lang.ast.AbstractTokenManager#getFileName(java.lang.String) %}
|
||||
|
||||
### External Contributions
|
||||
|
||||
@ -108,6 +131,9 @@ implementations, and their corresponding Parser if it exists (in the same packag
|
||||
* [#2314](https://github.com/pmd/pmd/pull/2314): \[doc] maven integration - Add version to plugin - [Pham Hai Trung](https://github.com/gpbp)
|
||||
* [#2353](https://github.com/pmd/pmd/pull/2353): \[plsql] xmlforest with optional AS - [Piotr Szymanski](https://github.com/szyman23)
|
||||
* [#2383](https://github.com/pmd/pmd/pull/2383): \[apex] Fix invalid apex in documentation - [Gwilym Kuiper](https://github.com/gwilymatgearset)
|
||||
* [#2395](https://github.com/pmd/pmd/pull/2395): \[apex] New Rule: Unused local variables - [Gwilym Kuiper](https://github.com/gwilymatgearset)
|
||||
* [#2396](https://github.com/pmd/pmd/pull/2396): \[apex] New rule: field declarations should be at start - [Gwilym Kuiper](https://github.com/gwilymatgearset)
|
||||
* [#2397](https://github.com/pmd/pmd/pull/2397): \[apex] fixed WITH SECURITY_ENFORCED regex to recognise line break characters - [Kieran Black](https://github.com/kieranlblack)
|
||||
|
||||
{% endtocmaker %}
|
||||
|
||||
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.apex.rule.bestpractices;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTBlockStatement;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTVariableDeclaration;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTVariableExpression;
|
||||
import net.sourceforge.pmd.lang.apex.rule.AbstractApexRule;
|
||||
|
||||
public class UnusedLocalVariableRule extends AbstractApexRule {
|
||||
public UnusedLocalVariableRule() {
|
||||
addRuleChainVisit(ASTVariableDeclaration.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(ASTVariableDeclaration node, Object data) {
|
||||
String variableName = node.getImage();
|
||||
|
||||
ASTBlockStatement variableContext = node.getFirstParentOfType(ASTBlockStatement.class);
|
||||
List<ASTVariableExpression> potentialUsages = variableContext.findDescendantsOfType(ASTVariableExpression.class);
|
||||
|
||||
for (ASTVariableExpression usage : potentialUsages) {
|
||||
if (usage.getParent() == node) {
|
||||
continue;
|
||||
}
|
||||
if (usage.getImage().equals(variableName)) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
addViolation(data, node, variableName);
|
||||
return data;
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.apex.rule.codestyle;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTBlockStatement;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTField;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTMethod;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTProperty;
|
||||
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 FieldDeclarationsShouldBeAtStartRule extends AbstractApexRule {
|
||||
private static final Comparator<ApexNode<?>> NODE_BY_SOURCE_LOCATION_COMPARATOR =
|
||||
Comparator
|
||||
.<ApexNode<?>>comparingInt(ApexNode::getBeginLine)
|
||||
.thenComparing(ApexNode::getBeginColumn);
|
||||
public static final String STATIC_INITIALIZER_METHOD_NAME = "<clinit>";
|
||||
|
||||
public FieldDeclarationsShouldBeAtStartRule() {
|
||||
addRuleChainVisit(ASTUserClass.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(ASTUserClass node, Object data) {
|
||||
// Unfortunately the parser re-orders the AST to put field declarations before method declarations
|
||||
// so we have to rely on line numbers / positions to work out where the first non-field declaration starts
|
||||
// so we can check if the fields are in acceptable places.
|
||||
List<ASTField> fields = node.findChildrenOfType(ASTField.class);
|
||||
|
||||
List<ApexNode<?>> nonFieldDeclarations = new ArrayList<>();
|
||||
|
||||
nonFieldDeclarations.addAll(getMethodNodes(node));
|
||||
nonFieldDeclarations.addAll(node.findChildrenOfType(ASTUserClass.class));
|
||||
nonFieldDeclarations.addAll(node.findChildrenOfType(ASTProperty.class));
|
||||
nonFieldDeclarations.addAll(node.findChildrenOfType(ASTBlockStatement.class));
|
||||
|
||||
Optional<ApexNode<?>> firstNonFieldDeclaration = nonFieldDeclarations.stream()
|
||||
.filter(ApexNode::hasRealLoc)
|
||||
.min(NODE_BY_SOURCE_LOCATION_COMPARATOR);
|
||||
|
||||
if (!firstNonFieldDeclaration.isPresent()) {
|
||||
// there is nothing except field declaration, so that has to come first
|
||||
return data;
|
||||
}
|
||||
|
||||
for (ASTField field : fields) {
|
||||
if (NODE_BY_SOURCE_LOCATION_COMPARATOR.compare(field, firstNonFieldDeclaration.get()) > 0) {
|
||||
addViolation(data, field, field.getName());
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private List<ApexNode<?>> getMethodNodes(ASTUserClass node) {
|
||||
// The method <clinit> represents static initializer blocks, of which there can be many. The
|
||||
// <clinit> method doesn't contain location information, however the containing ASTBlockStatements do,
|
||||
// so we fetch them for that method only.
|
||||
return node.findChildrenOfType(ASTMethod.class).stream()
|
||||
.flatMap(method -> method.getImage().equals(STATIC_INITIALIZER_METHOD_NAME)
|
||||
? method.findChildrenOfType(ASTBlockStatement.class).stream() : Stream.of(method))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
@ -85,7 +85,7 @@ public class ApexCRUDViolationRule extends AbstractApexRule {
|
||||
|
||||
private static final String[] RESERVED_KEYS_FLS = new String[] { "Schema", S_OBJECT_TYPE, };
|
||||
|
||||
private static final Pattern WITH_SECURITY_ENFORCED = Pattern.compile("(?i).*[^']\\s*WITH\\s+SECURITY_ENFORCED\\s*[^']*");
|
||||
private static final Pattern WITH_SECURITY_ENFORCED = Pattern.compile("(?is).*[^']\\s*WITH\\s+SECURITY_ENFORCED\\s*[^']*");
|
||||
|
||||
private final Map<String, String> varToTypeMapping = new HashMap<>();
|
||||
private final ListMultimap<String, String> typeToDMLOperationMapping = ArrayListMultimap.create();
|
||||
|
@ -208,4 +208,25 @@ public class Foo {
|
||||
</example>
|
||||
</rule>
|
||||
|
||||
<rule name="UnusedLocalVariable"
|
||||
since="6.23.0"
|
||||
language="apex"
|
||||
message="Variable ''{0}'' defined but not used"
|
||||
class="net.sourceforge.pmd.lang.apex.rule.bestpractices.UnusedLocalVariableRule"
|
||||
externalInfoUrl="${pmd.website.baseurl}/pmd_rules_apex_bestpractices.html#unusedlocalvariable">
|
||||
<description>
|
||||
Detects when a local variable is declared and/or assigned but not used.
|
||||
</description>
|
||||
<example>
|
||||
<![CDATA[
|
||||
public Boolean bar(String z) {
|
||||
String x = 'some string'; // not used
|
||||
|
||||
String y = 'some other string'; // used in the next line
|
||||
return z.equals(y);
|
||||
}
|
||||
]]>
|
||||
</example>
|
||||
</rule>
|
||||
|
||||
</ruleset>
|
||||
|
@ -102,6 +102,30 @@ if (foo) { // preferred approach
|
||||
</example>
|
||||
</rule>
|
||||
|
||||
<rule name="FieldDeclarationsShouldBeAtStart"
|
||||
language="apex"
|
||||
since="6.23.0"
|
||||
message="Field declaration for ''{0}'' should be before method declarations in its class"
|
||||
class="net.sourceforge.pmd.lang.apex.rule.codestyle.FieldDeclarationsShouldBeAtStartRule"
|
||||
externalInfoUrl="${pmd.website.baseurl}/pmd_rules_apex_codestyle.html#fielddeclarationsshouldbeatstart">
|
||||
<description>
|
||||
Field declarations should appear before method declarations within a class.
|
||||
</description>
|
||||
<priority>3</priority>
|
||||
<example>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
public Integer someField; // good
|
||||
|
||||
public void someMethod() {
|
||||
}
|
||||
|
||||
public Integer anotherField; // bad
|
||||
}
|
||||
]]>
|
||||
</example>
|
||||
</rule>
|
||||
|
||||
<rule name="FieldNamingConventions"
|
||||
since="6.15.0"
|
||||
message="The {0} name ''{1}'' doesn''t match ''{2}''"
|
||||
@ -336,5 +360,4 @@ while (true) { // preferred approach
|
||||
]]>
|
||||
</example>
|
||||
</rule>
|
||||
|
||||
</ruleset>
|
||||
|
@ -0,0 +1,11 @@
|
||||
/*
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.apex.rule.bestpractices;
|
||||
|
||||
import net.sourceforge.pmd.testframework.PmdRuleTst;
|
||||
|
||||
public class UnusedLocalVariableTest extends PmdRuleTst {
|
||||
// no additional unit tests
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.apex.rule.codestyle;
|
||||
|
||||
import net.sourceforge.pmd.testframework.PmdRuleTst;
|
||||
|
||||
public class FieldDeclarationsShouldBeAtStartTest extends PmdRuleTst {
|
||||
// no additional unit tests
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<test-data
|
||||
xmlns="http://pmd.sourceforge.net/rule-tests"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://pmd.sourceforge.net/rule-tests http://pmd.sourceforge.net/rule-tests_1_0_0.xsd">
|
||||
<test-code>
|
||||
<description>Unused variables should result in errors</description>
|
||||
<expected-problems>2</expected-problems>
|
||||
<expected-linenumbers>3,7</expected-linenumbers>
|
||||
<expected-messages>
|
||||
<message>Variable 'foo' defined but not used</message>
|
||||
<message>Variable 'foo' defined but not used</message>
|
||||
</expected-messages>
|
||||
<code>
|
||||
<![CDATA[
|
||||
public class Foo {
|
||||
public void assignedVariable() {
|
||||
String foo = 'unused string';
|
||||
}
|
||||
|
||||
public void justADeclaration() {
|
||||
String foo;
|
||||
}
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Used variables should not result in errors</description>
|
||||
<expected-problems>0</expected-problems>
|
||||
<code>
|
||||
<![CDATA[
|
||||
public class Foo {
|
||||
public String basicUsage() {
|
||||
String x = 'used variable';
|
||||
return x;
|
||||
}
|
||||
|
||||
public Account moreComplexUsage() {
|
||||
String x = 'blah';
|
||||
return [SELECT Id FROM Account WHERE Name = :x];
|
||||
}
|
||||
|
||||
public String usageInBlocks(Boolean y) {
|
||||
String x = 'used variable';
|
||||
|
||||
if (y) {
|
||||
return x;
|
||||
} else {
|
||||
return 'some other string';
|
||||
}
|
||||
}
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Shadowing a field</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>5</expected-linenumbers>
|
||||
<code><![CDATA[
|
||||
public class Foo {
|
||||
private String myfield;
|
||||
|
||||
public void unused() {
|
||||
String myfield = 'unused string';
|
||||
}
|
||||
|
||||
public String usedDifferentMethod() {
|
||||
String myfield = 'used';
|
||||
return myfield;
|
||||
}
|
||||
|
||||
public String fieldUsage() {
|
||||
return myfield;
|
||||
}
|
||||
}
|
||||
]]></code>
|
||||
</test-code>
|
||||
</test-data>
|
@ -0,0 +1,207 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<test-data
|
||||
xmlns="http://pmd.sourceforge.net/rule-tests"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://pmd.sourceforge.net/rule-tests http://pmd.sourceforge.net/rule-tests_1_0_0.xsd">
|
||||
<test-code>
|
||||
<description>Does not warn if there are no methods</description>
|
||||
<expected-problems>0</expected-problems>
|
||||
<code>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
public Integer thisIsOkay;
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Does warn if a field is after a method</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>4</expected-linenumbers>
|
||||
<expected-messages>
|
||||
<message>Field declaration for 'thisIsNotOkay' should be before method declarations in its class</message>
|
||||
</expected-messages>
|
||||
<code>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
public void someMethod() {}
|
||||
|
||||
public Integer thisIsNotOkay;
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Warns if field is after constructor</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>6</expected-linenumbers>
|
||||
<expected-messages>
|
||||
<message>Field declaration for 'someField' should be before method declarations in its class</message>
|
||||
</expected-messages>
|
||||
<code>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
public Foo(Integer someValue) {
|
||||
someField = someValue;
|
||||
}
|
||||
|
||||
private Integer someField;
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Warns only for fields after the first method declaration</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>8</expected-linenumbers>
|
||||
<expected-messages>
|
||||
<message>Field declaration for 'thisFieldIsNotOkay' should be before method declarations in its class</message>
|
||||
</expected-messages>
|
||||
<code>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
private Integer thisFieldIsOkay;
|
||||
|
||||
public Foo(Integer someValue) {
|
||||
someField = someValue;
|
||||
}
|
||||
|
||||
private Integer thisFieldIsNotOkay;
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Warns for fields defined on the same line after a method</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>2</expected-linenumbers>
|
||||
<expected-messages>
|
||||
<message>Field declaration for 'thisFieldIsNotOkay' should be before method declarations in its class</message>
|
||||
</expected-messages>
|
||||
<code>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
public Foo(Integer someValue) { someField = someValue; } private Integer thisFieldIsNotOkay;
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Does not warn for fields defined on the same line before a method</description>
|
||||
<expected-problems>0</expected-problems>
|
||||
<code>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
private Integer thisFieldIsOkay; public Foo(Integer someValue) { someField = someValue; }
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Allows nested classes to have fields</description>
|
||||
<expected-problems>0</expected-problems>
|
||||
<code>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
void bar() { }
|
||||
|
||||
private class InnerFoo {
|
||||
public Integer thisIsOkay;
|
||||
}
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Allows nested classes to have fields</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>9</expected-linenumbers>
|
||||
<code>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
void bar() { }
|
||||
|
||||
private class InnerFoo {
|
||||
public Integer thisIsOkay;
|
||||
|
||||
public void bar() {}
|
||||
|
||||
public Integer thisIsNotOkay;
|
||||
}
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Fields should go before inner classes too</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>4</expected-linenumbers>
|
||||
<code>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
private class InnerFoo {}
|
||||
|
||||
public Integer thisIsNotOkay;
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Fields should go before properties too</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>4</expected-linenumbers>
|
||||
<code>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
public Integer someProperty { get; }
|
||||
|
||||
public Integer thisIsNotOkay;
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Fields should go before block statements</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>6</expected-linenumbers>
|
||||
<code>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
{
|
||||
System.debug('Hello');
|
||||
}
|
||||
|
||||
public Integer thisIsNotOkay;
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Fields should go before static block statements</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>6</expected-linenumbers>
|
||||
<code>
|
||||
<![CDATA[
|
||||
class Foo {
|
||||
static {
|
||||
System.debug('Hello');
|
||||
}
|
||||
|
||||
public Integer thisIsNotOkay;
|
||||
}
|
||||
]]>
|
||||
</code>
|
||||
</test-code>
|
||||
</test-data>
|
@ -275,6 +275,19 @@ public class Foo {
|
||||
} ]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Accepts Closure SECURITY ENFORCED Line Break </description>
|
||||
<expected-problems>0</expected-problems>
|
||||
<code><![CDATA[
|
||||
public class Foo {
|
||||
public Contact foo(String tempID) {
|
||||
Contact c = [SELECT Name FROM Contact WHERE Id=: tempID
|
||||
WITH SECURITY_ENFORCED];
|
||||
return c;
|
||||
}
|
||||
} ]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Accepts Closure SECURITY ENFORCED in a List </description>
|
||||
<expected-problems>0</expected-problems>
|
||||
@ -287,6 +300,19 @@ public class Foo {
|
||||
} ]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Accepts Closure SECURITY ENFORCED in a List Line Break</description>
|
||||
<expected-problems>0</expected-problems>
|
||||
<code><![CDATA[
|
||||
public class Foo {
|
||||
public List<Contact> m() {
|
||||
List<Contact> c = [SELECT Name FROM Contact
|
||||
WITH SECURITY_ENFORCED LIMIT 1];
|
||||
return c;
|
||||
}
|
||||
} ]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Accepts Closure SECURITY ENFORCED with Case Insensitivity </description>
|
||||
<expected-problems>0</expected-problems>
|
||||
@ -299,6 +325,19 @@ public class Foo {
|
||||
} ]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Accepts Closure SECURITY ENFORCED with Case Insensitivity Line Break </description>
|
||||
<expected-problems>0</expected-problems>
|
||||
<code><![CDATA[
|
||||
public class Foo {
|
||||
public Contact foo(String tempID) {
|
||||
Contact c = [SELECT Name FROM Contact WHERE Id=: tempID
|
||||
WItH SECURITY_ENFORCED];
|
||||
return c;
|
||||
}
|
||||
} ]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Accepts Closure SECURITY ENFORCED Not Secured </description>
|
||||
<expected-problems>1</expected-problems>
|
||||
@ -323,6 +362,19 @@ public class Foo {
|
||||
} ]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Accepts Closure SECURITY ENFORCED Secured Line Break </description>
|
||||
<expected-problems>0</expected-problems>
|
||||
<code><![CDATA[
|
||||
public class Foo {
|
||||
public Contact foo() {
|
||||
Contact c = [SELECT Name FROM Contact WHERE Name = 'WITH SECURITY_ENFORCED'
|
||||
WITH SECURITY_ENFORCED];
|
||||
return c;
|
||||
}
|
||||
} ]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Proper accessibility CRUD,FLS </description>
|
||||
<expected-problems>0</expected-problems>
|
||||
|
@ -36,7 +36,9 @@ public interface Parser {
|
||||
* @param source
|
||||
* Reader that provides the source code to tokenize.
|
||||
* @return A TokenManager for reading token.
|
||||
* @deprecated For removal in 7.0.0
|
||||
*/
|
||||
@Deprecated
|
||||
TokenManager getTokenManager(String fileName, Reader source);
|
||||
|
||||
|
||||
|
@ -11,5 +11,10 @@ public interface TokenManager {
|
||||
// TODO : Change the return to GenericToken in 7.0.0 - maybe even use generics TokenManager<T extends GenericToken>
|
||||
Object getNextToken();
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated For removal in 7.0.0
|
||||
*/
|
||||
@Deprecated
|
||||
void setFileName(String fileName);
|
||||
}
|
||||
|
@ -19,10 +19,18 @@ public abstract class AbstractTokenManager {
|
||||
protected Map<Integer, String> suppressMap = new HashMap<>();
|
||||
protected String suppressMarker = PMD.SUPPRESS_MARKER;
|
||||
|
||||
/**
|
||||
* @deprecated For removal in 7.0.0
|
||||
*/
|
||||
@Deprecated
|
||||
public static void setFileName(String fileName) {
|
||||
AbstractTokenManager.fileName.set(fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated For removal in 7.0.0
|
||||
*/
|
||||
@Deprecated
|
||||
public static String getFileName() {
|
||||
String fileName = AbstractTokenManager.fileName.get();
|
||||
return fileName == null ? "(no file name provided)" : fileName;
|
||||
|
14
pmd-core/src/main/resources/rulesets/releases/6230.xml
Normal file
14
pmd-core/src/main/resources/rulesets/releases/6230.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<ruleset name="6230"
|
||||
xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">
|
||||
<description>
|
||||
This ruleset contains links to rules that are new in PMD v6.23.0
|
||||
</description>
|
||||
|
||||
<rule ref="category/apex/bestpractices.xml/UnusedLocalVariable"/>
|
||||
<rule ref="category/apex/codestyle.xml/FieldDeclarationsShouldBeAtStart"/>
|
||||
|
||||
</ruleset>
|
@ -290,6 +290,53 @@ class JavaParserImpl {
|
||||
return getToken(1).kind == IDENTIFIER && getToken(1).getImage().equals(keyword);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if we're in a switch block, one precondition for parsing a yield
|
||||
* statement.
|
||||
*/
|
||||
private boolean inSwitchExprBlock = false;
|
||||
|
||||
private boolean isYieldStart() {
|
||||
return inSwitchExprBlock
|
||||
&& isKeyword("yield")
|
||||
&& mayStartExprAfterYield(2);
|
||||
}
|
||||
|
||||
private boolean mayStartExprAfterYield(final int offset) {
|
||||
// based off of https://hg.openjdk.java.net/jdk/jdk/file/bc3da0226ffa/src/jdk.compiler/share/classes/com/sun/tools/javac/parser/JavacParser.java#l2580
|
||||
// please don't sue me
|
||||
Token token = getToken(offset);
|
||||
if (token == null) return false; // eof
|
||||
switch (token.kind) {
|
||||
case PLUS: case MINUS: case STRING_LITERAL: case CHARACTER_LITERAL:
|
||||
case INTEGER_LITERAL: case FLOATING_POINT_LITERAL: case HEX_FLOATING_POINT_LITERAL:
|
||||
case NULL: case IDENTIFIER: case TRUE: case FALSE:
|
||||
case NEW: case SWITCH: case THIS: case SUPER:
|
||||
return true;
|
||||
case INCR: case DECR:
|
||||
return getToken(offset + 1).kind != SEMICOLON; // eg yield++;
|
||||
case LPAREN:
|
||||
int lookahead = offset + 1;
|
||||
int balance = 1;
|
||||
Token t;
|
||||
while ((t = getToken(lookahead)) != null && balance > 0) {
|
||||
switch (t.kind) {
|
||||
case LPAREN: balance++; break;
|
||||
case RPAREN: balance--; break;
|
||||
case COMMA: if (balance == 1) return false; // a method call, eg yield(1, 2);
|
||||
}
|
||||
lookahead++;
|
||||
}
|
||||
// lambda: yield () -> {};
|
||||
// method call: yield ();
|
||||
return t != null
|
||||
&& (lookahead != offset + 2 // ie ()
|
||||
|| t.kind == LAMBDA);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldStartStatementInSwitch() {
|
||||
switch (getToken(1).kind) {
|
||||
case _DEFAULT:
|
||||
@ -1650,9 +1697,12 @@ void PostfixExpression() #void:
|
||||
|
||||
|
||||
void SwitchExpression() :
|
||||
{}
|
||||
{boolean prevInSwitchBlock = inSwitchExprBlock;}
|
||||
{
|
||||
"switch" "(" Expression() ")" SwitchBlock()
|
||||
"switch" "(" Expression() ")"
|
||||
{inSwitchExprBlock = true;}
|
||||
SwitchBlock()
|
||||
{inSwitchExprBlock = prevInSwitchBlock;}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1985,6 +2035,7 @@ void Statement() #void:
|
||||
{}
|
||||
{
|
||||
Block()
|
||||
| LOOKAHEAD( { isYieldStart() } ) YieldStatement()
|
||||
| EmptyStatement()
|
||||
| SwitchStatement()
|
||||
| IfStatement()
|
||||
@ -2021,7 +2072,7 @@ void BlockStatement() #void:
|
||||
{}
|
||||
{
|
||||
LOOKAHEAD( { isNextTokenAnAssert() } ) AssertStatement()
|
||||
| LOOKAHEAD({ jdkVersion >= 13 && isKeyword("yield") }) YieldStatement()
|
||||
| LOOKAHEAD( { isYieldStart() } ) YieldStatement()
|
||||
|
|
||||
LOOKAHEAD(( "final" | Annotation() )* Type() <IDENTIFIER>)
|
||||
LocalVariableDeclaration() ";" {
|
||||
|
@ -54,6 +54,11 @@ public final class ASTCompilationUnit extends AbstractJavaTypeNode implements Ro
|
||||
return AstImplUtil.getChildAs(this, 0, ASTPackageDeclaration.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ASTCompilationUnit getRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the package name of this compilation unit. If there is no
|
||||
* package declaration, then returns the empty string.
|
||||
@ -80,18 +85,12 @@ public final class ASTCompilationUnit extends AbstractJavaTypeNode implements Ro
|
||||
return classTypeResolver;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ASTCompilationUnit getRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull JSymbolTable getSymbolTable() {
|
||||
assert symbolTable != null : "Symbol table wasn't set";
|
||||
return symbolTable;
|
||||
}
|
||||
|
||||
@InternalApi
|
||||
@Deprecated
|
||||
public void setClassTypeResolver(ClassTypeResolver classTypeResolver) {
|
||||
|
@ -66,7 +66,7 @@ public class DuplicateImportsRule extends AbstractJavaRule {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
Class<?> importClass = node.getClassTypeResolver().loadClass(thisImportOnDemand.getName());
|
||||
Class<?> importClass = node.getClassTypeResolver().loadClassOrNull(thisImportOnDemand.getName());
|
||||
if (importClass != null) {
|
||||
for (Method m : importClass.getMethods()) {
|
||||
if (Modifier.isStatic(m.getModifiers()) && m.getName().equals(singleTypeName)) {
|
||||
|
@ -82,6 +82,7 @@ import net.sourceforge.pmd.lang.java.ast.TypeNode;
|
||||
import net.sourceforge.pmd.lang.java.ast.UnaryOp;
|
||||
import net.sourceforge.pmd.lang.java.symboltable.ClassScope;
|
||||
import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration;
|
||||
import net.sourceforge.pmd.lang.java.typeresolution.internal.NullableClassLoader;
|
||||
import net.sourceforge.pmd.lang.java.typeresolution.typedefinition.JavaTypeDefinition;
|
||||
import net.sourceforge.pmd.lang.symboltable.NameOccurrence;
|
||||
import net.sourceforge.pmd.lang.symboltable.Scope;
|
||||
@ -95,7 +96,7 @@ import net.sourceforge.pmd.lang.symboltable.Scope;
|
||||
|
||||
@Deprecated
|
||||
@InternalApi
|
||||
public class ClassTypeResolver extends JavaParserVisitorAdapter {
|
||||
public class ClassTypeResolver extends JavaParserVisitorAdapter implements NullableClassLoader {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ClassTypeResolver.class.getName());
|
||||
|
||||
@ -1346,10 +1347,15 @@ public class ClassTypeResolver extends JavaParserVisitorAdapter {
|
||||
return pmdClassLoader.loadClassOrNull(fullyQualifiedClassName) != null;
|
||||
}
|
||||
|
||||
public Class<?> loadClass(String fullyQualifiedClassName) {
|
||||
@Override
|
||||
public Class<?> loadClassOrNull(String fullyQualifiedClassName) {
|
||||
return pmdClassLoader.loadClassOrNull(fullyQualifiedClassName);
|
||||
}
|
||||
|
||||
public Class<?> loadClass(String fullyQualifiedClassName) {
|
||||
return loadClassOrNull(fullyQualifiedClassName);
|
||||
}
|
||||
|
||||
private Class<?> processOnDemand(String qualifiedName) {
|
||||
for (String entry : importedOnDemand) {
|
||||
String fullClassName = entry + "." + qualifiedName;
|
||||
@ -1392,12 +1398,12 @@ public class ClassTypeResolver extends JavaParserVisitorAdapter {
|
||||
String strPackage = anImportDeclaration.getPackageName();
|
||||
if (anImportDeclaration.isStatic()) {
|
||||
if (anImportDeclaration.isImportOnDemand()) {
|
||||
importOnDemandStaticClasses.add(JavaTypeDefinition.forClass(loadClass(strPackage)));
|
||||
importOnDemandStaticClasses.add(JavaTypeDefinition.forClass(loadClassOrNull(strPackage)));
|
||||
} else { // not import on-demand
|
||||
String strName = anImportDeclaration.getImportedName();
|
||||
String fieldName = strName.substring(strName.lastIndexOf('.') + 1);
|
||||
|
||||
Class<?> staticClassWithField = loadClass(strPackage);
|
||||
Class<?> staticClassWithField = loadClassOrNull(strPackage);
|
||||
if (staticClassWithField != null) {
|
||||
JavaTypeDefinition typeDef = getFieldType(JavaTypeDefinition.forClass(staticClassWithField),
|
||||
fieldName, currentAcu.getType());
|
||||
|
@ -15,6 +15,7 @@ import java.util.concurrent.ConcurrentMap;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
|
||||
import net.sourceforge.pmd.annotation.InternalApi;
|
||||
import net.sourceforge.pmd.lang.java.typeresolution.internal.NullableClassLoader;
|
||||
import net.sourceforge.pmd.lang.java.typeresolution.visitors.PMDASMVisitor;
|
||||
|
||||
/*
|
||||
@ -36,7 +37,7 @@ import net.sourceforge.pmd.lang.java.typeresolution.visitors.PMDASMVisitor;
|
||||
*/
|
||||
@InternalApi
|
||||
@Deprecated
|
||||
public final class PMDASMClassLoader extends ClassLoader {
|
||||
public final class PMDASMClassLoader extends ClassLoader implements NullableClassLoader {
|
||||
|
||||
private static PMDASMClassLoader cachedPMDASMClassLoader;
|
||||
private static ClassLoader cachedClassLoader;
|
||||
@ -88,6 +89,7 @@ public final class PMDASMClassLoader extends ClassLoader {
|
||||
* Not throwing CNFEs to represent failure makes a huge performance
|
||||
* difference. Typeres as a whole is 2x faster.
|
||||
*/
|
||||
@Override
|
||||
public Class<?> loadClassOrNull(String name) {
|
||||
if (dontBother.containsKey(name)) {
|
||||
return null;
|
||||
|
@ -6,7 +6,10 @@ package net.sourceforge.pmd.lang.java.typeresolution;
|
||||
|
||||
import static net.sourceforge.pmd.util.CollectionUtil.any;
|
||||
|
||||
import org.apache.commons.lang3.ClassUtils;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTAnnotationTypeDeclaration;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration;
|
||||
@ -14,9 +17,27 @@ import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceType;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTEnumDeclaration;
|
||||
import net.sourceforge.pmd.lang.java.ast.TypeNode;
|
||||
import net.sourceforge.pmd.lang.java.symboltable.TypedNameDeclaration;
|
||||
import net.sourceforge.pmd.lang.java.typeresolution.internal.NullableClassLoader;
|
||||
import net.sourceforge.pmd.lang.java.typeresolution.internal.NullableClassLoader.ClassLoaderWrapper;
|
||||
|
||||
public final class TypeHelper {
|
||||
|
||||
/** Maps names of primitives to their corresponding primitive {@code Class}es. */
|
||||
private static final Map<String, Class<?>> PRIMITIVES_BY_NAME = new HashMap<>();
|
||||
|
||||
|
||||
static {
|
||||
PRIMITIVES_BY_NAME.put("boolean", Boolean.TYPE);
|
||||
PRIMITIVES_BY_NAME.put("byte", Byte.TYPE);
|
||||
PRIMITIVES_BY_NAME.put("char", Character.TYPE);
|
||||
PRIMITIVES_BY_NAME.put("short", Short.TYPE);
|
||||
PRIMITIVES_BY_NAME.put("int", Integer.TYPE);
|
||||
PRIMITIVES_BY_NAME.put("long", Long.TYPE);
|
||||
PRIMITIVES_BY_NAME.put("double", Double.TYPE);
|
||||
PRIMITIVES_BY_NAME.put("float", Float.TYPE);
|
||||
PRIMITIVES_BY_NAME.put("void", Void.TYPE);
|
||||
}
|
||||
|
||||
private TypeHelper() {
|
||||
// utility class
|
||||
}
|
||||
@ -35,6 +56,10 @@ public final class TypeHelper {
|
||||
* @return <code>true</code> if type node n is of type clazzName or a subtype of clazzName
|
||||
*/
|
||||
public static boolean isA(final TypeNode n, final String clazzName) {
|
||||
if (n.getType() != null && n.getType().isAnnotation()) {
|
||||
return isAnnotationSubtype(n.getType(), clazzName);
|
||||
}
|
||||
|
||||
final Class<?> clazz = loadClassWithNodeClassloader(n, clazzName);
|
||||
|
||||
if (clazz != null || n.getType() != null) {
|
||||
@ -48,6 +73,20 @@ public final class TypeHelper {
|
||||
return fallbackIsA(n, clazzName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the class n is a subtype of clazzName, given n
|
||||
* is an annotationt type.
|
||||
*/
|
||||
private static boolean isAnnotationSubtype(Class<?> n, String clazzName) {
|
||||
assert n != null && n.isAnnotation() : "Not an annotation type";
|
||||
// then, the supertype may only be Object, j.l.Annotation, or the class name
|
||||
// this avoids classloading altogether
|
||||
// this is used e.g. by the typeIs function in XPath
|
||||
return "java.lang.annotation.Annotation".equals(clazzName)
|
||||
|| "java.lang.Object".equals(clazzName)
|
||||
|| clazzName.equals(n.getName());
|
||||
}
|
||||
|
||||
private static boolean fallbackIsA(TypeNode n, String clazzName) {
|
||||
if (clazzName.equals(n.getImage()) || clazzName.endsWith("." + n.getImage())) {
|
||||
return true;
|
||||
@ -70,9 +109,11 @@ public final class TypeHelper {
|
||||
return "java.lang.Enum".equals(clazzName)
|
||||
// supertypes of Enum
|
||||
|| "java.lang.Comparable".equals(clazzName)
|
||||
|| "java.io.Serializable".equals(clazzName);
|
||||
|| "java.io.Serializable".equals(clazzName)
|
||||
|| "java.lang.Object".equals(clazzName);
|
||||
} else if (n instanceof ASTAnnotationTypeDeclaration) {
|
||||
return "java.lang.annotation.Annotation".equals(clazzName);
|
||||
return "java.lang.annotation.Annotation".equals(clazzName)
|
||||
|| "java.lang.Object".equals(clazzName);
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -89,6 +130,11 @@ public final class TypeHelper {
|
||||
* @throws NullPointerException if n is null
|
||||
*/
|
||||
public static boolean isExactlyA(final TypeNode n, final String clazzName) {
|
||||
if (n.getType() != null && n.getType().getName().equals(clazzName)) {
|
||||
// fast path avoiding classloading
|
||||
return true;
|
||||
}
|
||||
|
||||
final Class<?> clazz = loadClassWithNodeClassloader(n, clazzName);
|
||||
|
||||
if (clazz != null) {
|
||||
@ -100,34 +146,87 @@ public final class TypeHelper {
|
||||
|
||||
private static Class<?> loadClassWithNodeClassloader(final TypeNode n, final String clazzName) {
|
||||
if (n.getType() != null) {
|
||||
return loadClass(n.getType().getClassLoader(), clazzName);
|
||||
return loadClass(n.getRoot().getClassTypeResolver(), clazzName);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Class<?> loadClass(final ClassLoader nullableClassLoader, final String clazzName) {
|
||||
try {
|
||||
ClassLoader classLoader = nullableClassLoader;
|
||||
if (classLoader == null) {
|
||||
// Using the system classloader then
|
||||
classLoader = ClassLoader.getSystemClassLoader();
|
||||
|
||||
/**
|
||||
* Load a class. Supports loading array types like 'java.lang.String[]' and
|
||||
* converting a canonical name to a binary name (eg 'java.util.Map.Entry' ->
|
||||
* 'java.util.Map$Entry').
|
||||
*/
|
||||
// test only
|
||||
static Class<?> loadClass(NullableClassLoader ctr, String className) {
|
||||
return loadClassMaybeArray(ctr, StringUtils.deleteWhitespace(className));
|
||||
}
|
||||
|
||||
private static Class<?> loadClassFromCanonicalName(NullableClassLoader ctr, String className) {
|
||||
Class<?> clazz = PRIMITIVES_BY_NAME.get(className);
|
||||
if (clazz == null) {
|
||||
clazz = ctr.loadClassOrNull(className);
|
||||
}
|
||||
if (clazz != null) {
|
||||
return clazz;
|
||||
}
|
||||
// allow path separators (.) as inner class name separators
|
||||
final int lastDotIndex = className.lastIndexOf('.');
|
||||
|
||||
if (lastDotIndex >= 0) {
|
||||
String asInner = className.substring(0, lastDotIndex)
|
||||
+ '$' + className.substring(lastDotIndex + 1);
|
||||
return loadClassFromCanonicalName(ctr, asInner);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private static Class<?> loadClassMaybeArray(NullableClassLoader classLoader,
|
||||
String className) {
|
||||
Validate.notNull(className, "className must not be null.");
|
||||
if (className.endsWith("[]")) {
|
||||
int dimension = 0;
|
||||
int i = className.length();
|
||||
while (i >= 2 && className.startsWith("[]", i - 2)) {
|
||||
dimension++;
|
||||
i -= 2;
|
||||
}
|
||||
|
||||
// If the requested type is in the classpath, using the same classloader should work
|
||||
return ClassUtils.getClass(classLoader, clazzName);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// The requested type is not on the auxclasspath. This might happen, if the type node
|
||||
// is probed for a specific type (e.g. is is a JUnit5 Test Annotation class).
|
||||
// Failing to resolve clazzName does not necessarily indicate an incomplete auxclasspath.
|
||||
} catch (final LinkageError expected) {
|
||||
// We found the class but it's invalid / incomplete. This may be an incomplete auxclasspath
|
||||
// if it was a NoClassDefFoundError. TODO : Report it?
|
||||
checkJavaIdent(className, i);
|
||||
String elementName = className.substring(0, i);
|
||||
|
||||
Class<?> elementType = loadClassFromCanonicalName(classLoader, elementName);
|
||||
if (elementType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Array.newInstance(elementType, (int[]) Array.newInstance(int.class, dimension)).getClass();
|
||||
} else {
|
||||
checkJavaIdent(className, className.length());
|
||||
return loadClassFromCanonicalName(classLoader, className);
|
||||
}
|
||||
}
|
||||
|
||||
private static IllegalArgumentException invalidClassName(String className) {
|
||||
return new IllegalArgumentException("Not a valid class name \"" + className + "\"");
|
||||
}
|
||||
|
||||
private static void checkJavaIdent(String className, int endOffsetExclusive) {
|
||||
if (endOffsetExclusive <= 0 || !Character.isJavaIdentifierStart(className.charAt(0))) {
|
||||
throw invalidClassName(className);
|
||||
}
|
||||
|
||||
return null;
|
||||
for (int i = 1; i < endOffsetExclusive; i++) {
|
||||
char c = className.charAt(i);
|
||||
if (!(Character.isJavaIdentifierPart(c) || c == '.')) {
|
||||
throw invalidClassName(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @see #isA(TypeNode, String) */
|
||||
public static boolean isA(TypeNode n, Class<?> clazz) {
|
||||
return subclasses(n, clazz);
|
||||
@ -141,7 +240,7 @@ public final class TypeHelper {
|
||||
Class<?> type = vnd.getType();
|
||||
for (final Class<?> clazz : clazzes) {
|
||||
if (type != null && type.equals(clazz) || type == null
|
||||
&& (clazz.getSimpleName().equals(vnd.getTypeImage()) || clazz.getName().equals(vnd.getTypeImage()))) {
|
||||
&& (clazz.getSimpleName().equals(vnd.getTypeImage()) || clazz.getName().equals(vnd.getTypeImage()))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -191,7 +290,7 @@ public final class TypeHelper {
|
||||
public static boolean isA(TypedNameDeclaration vnd, String className) {
|
||||
Class<?> type = vnd.getType();
|
||||
if (type != null) {
|
||||
Class<?> clazz = loadClass(type.getClassLoader(), className);
|
||||
Class<?> clazz = loadClass(ClassLoaderWrapper.wrapNullable(type.getClassLoader()), className);
|
||||
if (clazz != null) {
|
||||
return clazz.isAssignableFrom(type);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user