Merge branch 'master' into issue-2755
This commit is contained in:
@ -3,6 +3,7 @@
|
||||
[](https://gitter.im/pmd/pmd?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://travis-ci.org/pmd/pmd)
|
||||
[](https://maven-badges.herokuapp.com/maven-central/net.sourceforge.pmd/pmd)
|
||||
[](https://github.com/jvm-repo-rebuild/reproducible-central#net.sourceforge.pmd:pmd)
|
||||
[](https://coveralls.io/github/pmd/pmd)
|
||||
[](https://www.codacy.com/app/pmd/pmd?utm_source=github.com&utm_medium=referral&utm_content=pmd/pmd&utm_campaign=Badge_Grade)
|
||||
[](code_of_conduct.md)
|
||||
|
@ -29,14 +29,19 @@ AbstractTokenizer and the custom tokenizers of Fortran, Perl and Ruby are deprec
|
||||
* [#2758](https://github.com/pmd/pmd/pull/2758): \[cpd] Improve AnyTokenizer
|
||||
* [#2760](https://github.com/pmd/pmd/issues/2760): \[cpd] AnyTokenizer doesn't count columns correctly
|
||||
|
||||
* pmd-java
|
||||
* [#2708](https://github.com/pmd/pmd/issues/2708): \[java] False positive FinalFieldCouldBeStatic when using lombok Builder.Default
|
||||
* apex-security
|
||||
* [#2774](https://github.com/pmd/pmd/issues/2774): \[apex] ApexSharingViolations does not correlate sharing settings with class that contains data access
|
||||
|
||||
* java
|
||||
* [#2738](https://github.com/pmd/pmd/issues/2738): \[java] Custom rule with @ExhaustiveEnumSwitch throws NPE
|
||||
* [#2755](https://github.com/pmd/pmd/issues/2755): \[java] \[6.27.0] Exception applying rule CloseResource on file ... java.lang.NullPointerException
|
||||
* [#2756](https://github.com/pmd/pmd/issues/2756): \[java] TypeTestUtil fails with NPE for anonymous class
|
||||
* [#2759](https://github.com/pmd/pmd/issues/2759): \[java] False positive in UnusedAssignment
|
||||
* [#2767](https://github.com/pmd/pmd/issues/2767): \[java] IndexOutOfBoundsException when parsing an initializer BlockStatement
|
||||
* [#2783](https://github.com/pmd/pmd/issues/2783): \[java] Error while parsing with lambda of custom interface
|
||||
* java-bestpractices
|
||||
* [#2759](https://github.com/pmd/pmd/issues/2759): \[java] False positive in UnusedAssignment
|
||||
* java-design
|
||||
* [#2708](https://github.com/pmd/pmd/issues/2708): \[java] False positive FinalFieldCouldBeStatic when using lombok Builder.Default
|
||||
|
||||
|
||||
### API Changes
|
||||
@ -69,6 +74,8 @@ AbstractTokenizer and the custom tokenizers of Fortran, Perl and Ruby are deprec
|
||||
* [#2735](https://github.com/pmd/pmd/pull/2735): \[ci] Add github actions for a fast view of pr succeed/not - [XenoAmess](https://github.com/XenoAmess)
|
||||
* [#2747](https://github.com/pmd/pmd/pull/2747): \[java] Don't trigger FinalFieldCouldBeStatic when field is annotated with lombok @Builder.Default - [Ollie Abbey](https://github.com/ollieabbey)
|
||||
* [#2773](https://github.com/pmd/pmd/pull/2773): \[java] issue-2738: Adding null check to avoid npe when switch case is default - [Nimit Patel](https://github.com/nimit-patel)
|
||||
* [#2789](https://github.com/pmd/pmd/pull/2789): Add badge for reproducible build - [Dan Rollo](https://github.com/bhamail)
|
||||
* [#2791](https://github.com/pmd/pmd/pull/2791): \[apex] Analyze inner classes for sharing violations - [Jeff Bartolotta](https://github.com/jbartolotta-sfdc)
|
||||
|
||||
{% endtocmaker %}
|
||||
|
||||
|
@ -4,11 +4,20 @@
|
||||
|
||||
package net.sourceforge.pmd.lang.apex.rule.security;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import net.sourceforge.pmd.RuleContext;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTDmlDeleteStatement;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTDmlInsertStatement;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTDmlMergeStatement;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTDmlUndeleteStatement;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTDmlUpdateStatement;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTDmlUpsertStatement;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTMethodCallExpression;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTModifierNode;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTSoqlExpression;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTSoslExpression;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ASTUserClass;
|
||||
import net.sourceforge.pmd.lang.apex.ast.ApexNode;
|
||||
import net.sourceforge.pmd.lang.apex.rule.AbstractApexRule;
|
||||
@ -21,45 +30,103 @@ import net.sourceforge.pmd.lang.apex.rule.internal.Helper;
|
||||
*/
|
||||
public class ApexSharingViolationsRule extends AbstractApexRule {
|
||||
|
||||
/**
|
||||
* Keep track of previously reported violations to avoid duplicates.
|
||||
*/
|
||||
private WeakHashMap<ApexNode<?>, Object> localCacheOfReportedNodes = new WeakHashMap<>();
|
||||
|
||||
public ApexSharingViolationsRule() {
|
||||
setProperty(CODECLIMATE_CATEGORIES, "Security");
|
||||
setProperty(CODECLIMATE_REMEDIATION_MULTIPLIER, 100);
|
||||
setProperty(CODECLIMATE_BLOCK_HIGHLIGHTING, false);
|
||||
addRuleChainVisit(ASTDmlDeleteStatement.class);
|
||||
addRuleChainVisit(ASTDmlInsertStatement.class);
|
||||
addRuleChainVisit(ASTDmlMergeStatement.class);
|
||||
addRuleChainVisit(ASTDmlUndeleteStatement.class);
|
||||
addRuleChainVisit(ASTDmlUpdateStatement.class);
|
||||
addRuleChainVisit(ASTDmlUpsertStatement.class);
|
||||
addRuleChainVisit(ASTMethodCallExpression.class);
|
||||
addRuleChainVisit(ASTSoqlExpression.class);
|
||||
addRuleChainVisit(ASTSoslExpression.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(ASTUserClass node, Object data) {
|
||||
|
||||
if (Helper.isTestMethodOrClass(node) || Helper.isSystemLevelClass(node)) {
|
||||
return data; // stops all the rules
|
||||
}
|
||||
|
||||
if (!Helper.isTestMethodOrClass(node)) {
|
||||
boolean sharingFound = isSharingPresent(node);
|
||||
checkForSharingDeclaration(node, data, sharingFound);
|
||||
checkForDatabaseMethods(node, data, sharingFound);
|
||||
}
|
||||
|
||||
public void start(RuleContext ctx) {
|
||||
super.start(ctx);
|
||||
localCacheOfReportedNodes.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(ASTSoqlExpression node, Object data) {
|
||||
checkForViolation(node, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if class contains any Database.query / Database.insert [ Database.*
|
||||
* ] methods
|
||||
*
|
||||
* @param node
|
||||
* @param data
|
||||
*/
|
||||
private void checkForDatabaseMethods(ASTUserClass node, Object data, boolean sharingFound) {
|
||||
List<ASTMethodCallExpression> calls = node.findDescendantsOfType(ASTMethodCallExpression.class);
|
||||
for (ASTMethodCallExpression call : calls) {
|
||||
if (Helper.isMethodName(call, "Database", Helper.ANY_METHOD)) {
|
||||
if (!sharingFound) {
|
||||
reportViolation(node, data);
|
||||
}
|
||||
@Override
|
||||
public Object visit(ASTSoslExpression node, Object data) {
|
||||
checkForViolation(node, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(ASTDmlUpsertStatement node, Object data) {
|
||||
checkForViolation(node, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(ASTDmlUpdateStatement node, Object data) {
|
||||
checkForViolation(node, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(ASTDmlUndeleteStatement node, Object data) {
|
||||
checkForViolation(node, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(ASTDmlMergeStatement node, Object data) {
|
||||
checkForViolation(node, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(ASTDmlInsertStatement node, Object data) {
|
||||
checkForViolation(node, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(ASTDmlDeleteStatement node, Object data) {
|
||||
checkForViolation(node, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(ASTMethodCallExpression node, Object data) {
|
||||
if (Helper.isMethodName(node, "Database", Helper.ANY_METHOD)) {
|
||||
checkForViolation(node, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private void checkForViolation(ApexNode<?> node, Object data) {
|
||||
// The closest ASTUserClass class in the tree hierarchy is the node that requires the sharing declaration
|
||||
ASTUserClass sharingDeclarationClass = node.getFirstParentOfType(ASTUserClass.class);
|
||||
|
||||
// This is null in the case of triggers
|
||||
if (sharingDeclarationClass != null) {
|
||||
// Apex allows a single level of class nesting. Check to see if sharingDeclarationClass has an outer class
|
||||
ASTUserClass outerClass = sharingDeclarationClass.getFirstParentOfType(ASTUserClass.class);
|
||||
// The test annotation needs to be on the outermost class
|
||||
ASTUserClass testAnnotationClass = Optional.ofNullable(outerClass).orElse(sharingDeclarationClass);
|
||||
|
||||
if (!Helper.isTestMethodOrClass(testAnnotationClass) && !Helper.isSystemLevelClass(sharingDeclarationClass) && !isSharingPresent(sharingDeclarationClass)) {
|
||||
// The violation is reported on the class, not the node that performs data access
|
||||
reportViolation(sharingDeclarationClass, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -77,19 +144,6 @@ public class ApexSharingViolationsRule extends AbstractApexRule {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if class has no sharing declared
|
||||
*
|
||||
* @param node
|
||||
* @param data
|
||||
*/
|
||||
private void checkForSharingDeclaration(ApexNode<?> node, Object data, boolean sharingFound) {
|
||||
final boolean foundAnyDMLorSOQL = Helper.foundAnyDML(node) || Helper.foundAnySOQLorSOSL(node);
|
||||
if (!sharingFound && !Helper.isTestMethodOrClass(node) && foundAnyDMLorSOQL) {
|
||||
reportViolation(node, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does class have sharing keyword declared?
|
||||
*
|
||||
|
@ -0,0 +1,194 @@
|
||||
/**
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.lang.apex.rule.security;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
import net.sourceforge.pmd.Report;
|
||||
import net.sourceforge.pmd.Rule;
|
||||
import net.sourceforge.pmd.RuleViolation;
|
||||
import net.sourceforge.pmd.lang.LanguageRegistry;
|
||||
import net.sourceforge.pmd.lang.apex.ApexLanguageModule;
|
||||
import net.sourceforge.pmd.testframework.RuleTst;
|
||||
|
||||
/**
|
||||
* <p>Sharing settings are not inherited by inner classes. Sharing settings need to be declared on the class that
|
||||
* contains the Database method, DML, SOQL, or SOSL.</p>
|
||||
*
|
||||
* <p>This test runs against Apex code that has an Outer class and and Inner class. Different Apex code is generated
|
||||
* based on the boolean permutations. Any classes that includes data access cod, but doesn't declare a sharing setting
|
||||
* should trigger a violation.</p>
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class ApexSharingViolationsNestedClassTest extends RuleTst {
|
||||
/**
|
||||
* Type of operation that may require a sharing declaration.
|
||||
*/
|
||||
private enum Operation {
|
||||
NONE(null),
|
||||
DML_DELETE("Contact c = new Contact(); delete c;"),
|
||||
DML_INSERT("Contact c = new Contact(); insert c;"),
|
||||
DML_MERGE("Contact c1 = new Contact(); Contact c2 = new Contact(); merge c1 c2;"),
|
||||
DML_UNDELETE("Contact c = new Contact(); undelete c;"),
|
||||
DML_UPDATE("Contact c = new Contact(); update c;"),
|
||||
DML_UPSERT("Contact c = new Contact(); upsert c;"),
|
||||
METHOD_DATABASE("Database.query('Select Id from Contact LIMIT 100');"),
|
||||
SOQL("[SELECT Name FROM Contact];"),
|
||||
SOSL("[FIND 'Foo' IN ALL FIELDS RETURNING Account(Name)];");
|
||||
|
||||
final boolean requiresSharingDeclaration;
|
||||
final String codeSnippet;
|
||||
|
||||
Operation(String codeSnippet) {
|
||||
this.requiresSharingDeclaration = codeSnippet != null;
|
||||
this.codeSnippet = codeSnippet;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The permutations used for class generation. See {@link #generateClass(boolean, Operation, boolean, Operation)}
|
||||
*/
|
||||
private final boolean outerSharingDeclared;
|
||||
private final Operation outerOperation;
|
||||
private final boolean innerSharingDeclared;
|
||||
private final Operation innerOperation;
|
||||
|
||||
/**
|
||||
* Track the expected violations based on the permutations.
|
||||
*/
|
||||
private final int expectedViolations;
|
||||
private final List<Integer> expectedLineNumbers;
|
||||
|
||||
public ApexSharingViolationsNestedClassTest(boolean outerSharingDeclared, Operation outerOperation,
|
||||
boolean innerSharingDeclared, Operation innerOperation,
|
||||
int expectedViolations, List<Integer> expectedLineNumbers) {
|
||||
this.outerSharingDeclared = outerSharingDeclared;
|
||||
this.outerOperation = outerOperation;
|
||||
this.innerSharingDeclared = innerSharingDeclared;
|
||||
this.innerOperation = innerOperation;
|
||||
this.expectedViolations = expectedViolations;
|
||||
this.expectedLineNumbers = expectedLineNumbers;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSharingPermutation() {
|
||||
String apexClass = generateClass(outerSharingDeclared, outerOperation, innerSharingDeclared, innerOperation);
|
||||
Report rpt = new Report();
|
||||
runTestFromString(apexClass, new ApexSharingViolationsRule(), rpt,
|
||||
LanguageRegistry.getLanguage(ApexLanguageModule.NAME).getDefaultVersion());
|
||||
List<RuleViolation> violations = rpt.getViolations();
|
||||
assertEquals("Unexpected Violation Size\n" + apexClass, expectedViolations, violations.size());
|
||||
List<Integer> lineNumbers = violations.stream().map(v -> v.getBeginLine()).collect(Collectors.toList());
|
||||
assertEquals("Unexpected Line Numbers\n" + apexClass, expectedLineNumbers, lineNumbers);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Rule> getRules() {
|
||||
Rule rule = findRule("category/apex/security.xml", "ApexSharingViolations");
|
||||
return Collections.singletonList(rule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameter provider that covers are all permutations
|
||||
*/
|
||||
@Parameterized.Parameters
|
||||
public static Collection<?> data() {
|
||||
List<Object[]> data = new ArrayList<>();
|
||||
|
||||
boolean[] boolPermutations = {false, true};
|
||||
|
||||
for (boolean outerSharingDeclared : boolPermutations) {
|
||||
for (Operation outerOperation : Operation.values()) {
|
||||
for (boolean innerSharingDeclared : boolPermutations) {
|
||||
for (Operation innerOperation : Operation.values()) {
|
||||
int expectedViolations = 0;
|
||||
List<Integer> expectedLineNumbers = new ArrayList<>();
|
||||
if (outerOperation.requiresSharingDeclaration && !outerSharingDeclared) {
|
||||
// The outer class contains SOQL but doesn't declare sharing
|
||||
expectedViolations++;
|
||||
expectedLineNumbers.add(1);
|
||||
}
|
||||
|
||||
if (innerOperation.requiresSharingDeclaration && !innerSharingDeclared) {
|
||||
// The inner class contains SOQL but doesn't declare sharing
|
||||
expectedViolations++;
|
||||
// The location of the inner class declaration depends upon the content of the outer class
|
||||
expectedLineNumbers.add(outerOperation.requiresSharingDeclaration ? 3 : 2);
|
||||
}
|
||||
data.add(new Object[]{outerSharingDeclared, outerOperation, innerSharingDeclared, innerOperation,
|
||||
expectedViolations, expectedLineNumbers});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Generate an Apex class with various Sharing/Database/DML/SOQL/SOSL permutations. An example of the class
|
||||
* returned when invoked with generateClass(true, SOQL, true, SOQL).</p>
|
||||
*
|
||||
* <pre>
|
||||
* public with sharing class Outer {
|
||||
* public void outerSOQL() {[SELECT Name FROM Contact];}
|
||||
* public with sharing class Inner {
|
||||
* public void innerSOQL() {[SELECT Name FROM Contact];}
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param outerSharing Add 'with sharing' to Outer class definition
|
||||
* @param outerOperation Add a method to Outer class that performs the given operation
|
||||
* @param innerSharing Add 'with sharing' to Inner class definition
|
||||
* @param innerOperation Add a method to Inner class that performs the given operation
|
||||
* @return String that represents Apex code
|
||||
*/
|
||||
private static String generateClass(boolean outerSharing, Operation outerOperation, boolean innerSharing,
|
||||
Operation innerOperation) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("public ");
|
||||
if (outerSharing) {
|
||||
sb.append("with sharing ");
|
||||
}
|
||||
sb.append("class Outer {\n");
|
||||
switch (outerOperation) {
|
||||
case NONE:
|
||||
// Do nothing
|
||||
break;
|
||||
default:
|
||||
sb.append(String.format("\t\tpublic void outer%s(){ %s }\n", outerOperation.name(), outerOperation.codeSnippet));
|
||||
break;
|
||||
}
|
||||
sb.append("\tpublic ");
|
||||
if (innerSharing) {
|
||||
sb.append("with sharing ");
|
||||
}
|
||||
sb.append("class Inner {\n");
|
||||
switch (innerOperation) {
|
||||
case NONE:
|
||||
// DO Nothing
|
||||
break;
|
||||
default:
|
||||
sb.append(String.format("\t\tpublic void inner%s(){ %s }\n", innerOperation.name(), innerOperation.codeSnippet));
|
||||
break;
|
||||
}
|
||||
sb.append("\t}\n"); // Closes class Inner
|
||||
sb.append("}\n"); // Closes class Outer
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@ -99,6 +99,87 @@ public inherited sharing class MyClass {
|
||||
public List<Contact> getAllTheSecrets() {
|
||||
return [SELECT Name FROM Contact];
|
||||
}
|
||||
}
|
||||
]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Apex test classes do not need a sharing declaration</description>
|
||||
<expected-problems>0</expected-problems>
|
||||
<code><![CDATA[
|
||||
@isTest public class MyClass {
|
||||
public List<Contact> getAllTheSecrets() {
|
||||
return [SELECT Name FROM Contact];
|
||||
}
|
||||
}
|
||||
]]></code>
|
||||
</test-code>
|
||||
|
||||
<!-- @isTest can only be declared on the outer class, it is inherited by the inner class-->
|
||||
<test-code>
|
||||
<description>Apex inner test classes do not need a sharing declaration</description>
|
||||
<expected-problems>0</expected-problems>
|
||||
<code><![CDATA[
|
||||
@isTest public class Outer {
|
||||
public List<Contact> getAllTheOuterSecrets() {
|
||||
return [SELECT Name FROM Contact];
|
||||
}
|
||||
public class Inner {
|
||||
public List<Contact> getAllTheInnerSecrets() {
|
||||
return [SELECT Name FROM Contact];
|
||||
}
|
||||
}
|
||||
}
|
||||
]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Nested method calls are detected</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<code><![CDATA[
|
||||
public class Dao {
|
||||
static Map<String, List<SObject>> recordsMap = new Map<String, List<SObject>>();
|
||||
|
||||
public List<SObject> getRecords(String query) {
|
||||
if (!recordsMap.containsKey(query)) {
|
||||
recordsMap.put(query, Database.query(query));
|
||||
}
|
||||
return recordsMap.get(query);
|
||||
}
|
||||
}
|
||||
]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Trigger does not require sharing</description>
|
||||
<expected-problems>0</expected-problems>
|
||||
<code><![CDATA[
|
||||
trigger MyTrigger on Account(after insert, after update) {
|
||||
[SELECT Id,(SELECT Id FROM Opportunities) FROM Account WHERE Id IN :Trigger.New];
|
||||
}
|
||||
]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Issue #2774(false positive). Sharing correctly declared on inner class.</description>
|
||||
<expected-problems>0</expected-problems>
|
||||
<code><![CDATA[
|
||||
public class Outer {
|
||||
public with sharing class Inner {
|
||||
public List<Contact> getAllInnerSoqlSecrets() { return [SELECT Name FROM Contact]; }
|
||||
}
|
||||
}
|
||||
]]></code>
|
||||
</test-code>
|
||||
|
||||
<test-code>
|
||||
<description>Issue #2774(false negative). Sharing incorrectly declared on outer class.</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<code><![CDATA[
|
||||
public with sharing class Outer {
|
||||
public class Inner {
|
||||
public List<Contact> getAllInnerSoqlSecrets() { return [SELECT Name FROM Contact]; }
|
||||
}
|
||||
}
|
||||
]]></code>
|
||||
</test-code>
|
||||
|
@ -4,10 +4,9 @@
|
||||
|
||||
package net.sourceforge.pmd.lang.java.types;
|
||||
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.List;
|
||||
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTAnnotationTypeDeclaration;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTAnyTypeDeclaration;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration;
|
||||
@ -42,10 +41,10 @@ public final class TypeTestUtil {
|
||||
* if the type of the node is parameterized. Examples:
|
||||
*
|
||||
* <pre>{@code
|
||||
* isA(<new ArrayList<String>()>, List.class) = true
|
||||
* isA(<new ArrayList<String>()>, ArrayList.class) = true
|
||||
* isA(<new int[0]>, int[].class) = true
|
||||
* isA(<new String[0]>, Object[].class) = true
|
||||
* isA(List.class, <new ArrayList<String>()>) = true
|
||||
* isA(ArrayList.class, <new ArrayList<String>()>) = true
|
||||
* isA(int[].class, <new int[0]>) = true
|
||||
* isA(Object[].class, <new String[0]>) = true
|
||||
* isA(_, null) = false
|
||||
* isA(null, _) = NullPointerException
|
||||
* }</pre>
|
||||
@ -65,8 +64,11 @@ public final class TypeTestUtil {
|
||||
return true;
|
||||
}
|
||||
|
||||
return canBeExtended(clazz) ? isA(clazz.getName(), node)
|
||||
: isExactlyA(clazz, node);
|
||||
if (hasNoSubtypes(clazz)) {
|
||||
return isExactlyA(clazz, node);
|
||||
}
|
||||
String canoName = clazz.getCanonicalName();
|
||||
return canoName != null && isA(canoName, node);
|
||||
}
|
||||
|
||||
|
||||
@ -76,10 +78,10 @@ public final class TypeTestUtil {
|
||||
* if the type of the node is parameterized. Examples:
|
||||
*
|
||||
* <pre>{@code
|
||||
* isA(<new ArrayList<String>()>, "java.util.List") = true
|
||||
* isA(<new ArrayList<String>()>, "java.util.ArrayList") = true
|
||||
* isA(<new int[0]>, "int[]") = true
|
||||
* isA(<new String[0]>, "java.lang.Object[]") = true
|
||||
* isA("java.util.List", <new ArrayList<String>()>) = true
|
||||
* isA("java.util.ArrayList", <new ArrayList<String>()>) = true
|
||||
* isA("int[]", <new int[0]>) = true
|
||||
* isA("java.lang.Object[]", <new String[0]>) = true
|
||||
* isA(_, null) = false
|
||||
* isA(null, _) = NullPointerException
|
||||
* }</pre>
|
||||
@ -156,10 +158,10 @@ public final class TypeTestUtil {
|
||||
* if the type of the node is parameterized.
|
||||
*
|
||||
* <pre>{@code
|
||||
* isExactlyA(<new ArrayList<String>()>, List.class) = false
|
||||
* isExactlyA(<new ArrayList<String>()>, ArrayList.class) = true
|
||||
* isExactlyA(<new int[0]>, int[].class) = true
|
||||
* isExactlyA(<new String[0]>, Object[].class) = false
|
||||
* isExactlyA(List.class, <new ArrayList<String>()>) = false
|
||||
* isExactlyA(ArrayList.class, <new ArrayList<String>()>) = true
|
||||
* isExactlyA(int[].class, <new int[0]>) = true
|
||||
* isExactlyA(Object[].class, <new String[0]>) = false
|
||||
* isExactlyA(_, null) = false
|
||||
* isExactlyA(null, _) = NullPointerException
|
||||
* }</pre>
|
||||
@ -198,9 +200,12 @@ public final class TypeTestUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean canBeExtended(Class<?> clazz) {
|
||||
|
||||
private static boolean hasNoSubtypes(Class<?> clazz) {
|
||||
// Neither final nor an annotation. Enums & records have ACC_FINAL
|
||||
return (clazz.getModifiers() & (Opcodes.ACC_ANNOTATION | Opcodes.ACC_FINAL)) == 0;
|
||||
// Note: arrays have ACC_FINAL, but have subtypes by covariance
|
||||
// Note: annotations may be implemented by classes
|
||||
return Modifier.isFinal(clazz.getModifiers()) && !clazz.isArray();
|
||||
}
|
||||
|
||||
// those fallbacks can be removed with the newer symbol resolution,
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
package net.sourceforge.pmd.lang.java.types;
|
||||
|
||||
import java.io.ObjectStreamField;
|
||||
import java.io.Serializable;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.concurrent.Callable;
|
||||
@ -20,6 +21,7 @@ import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTEnumDeclaration;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTMarkerAnnotation;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTName;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTType;
|
||||
import net.sourceforge.pmd.lang.java.ast.TypeNode;
|
||||
import net.sourceforge.pmd.lang.java.symboltable.BaseNonParserTest;
|
||||
import net.sourceforge.pmd.lang.java.types.testdata.SomeClassWithAnon;
|
||||
@ -56,12 +58,60 @@ public class TypeTestUtilTest extends BaseNonParserTest {
|
||||
|
||||
Assert.assertNull(klass.getType());
|
||||
Assert.assertTrue(TypeTestUtil.isA("org.FooBar", klass));
|
||||
assertIsA(klass, Iterable.class);
|
||||
assertIsA(klass, Enum.class);
|
||||
assertIsA(klass, Serializable.class);
|
||||
assertIsA(klass, Object.class);
|
||||
assertIsStrictSubtype(klass, Iterable.class);
|
||||
assertIsStrictSubtype(klass, Enum.class);
|
||||
assertIsStrictSubtype(klass, Serializable.class);
|
||||
assertIsStrictSubtype(klass, Object.class);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testIsAnArrayClass() {
|
||||
|
||||
ASTType arrayT =
|
||||
java.parse("import java.io.ObjectStreamField; "
|
||||
+ "class Foo { private static final ObjectStreamField[] serialPersistentFields; }")
|
||||
.getFirstDescendantOfType(ASTType.class);
|
||||
|
||||
|
||||
assertIsExactlyA(arrayT, ObjectStreamField[].class);
|
||||
assertIsStrictSubtype(arrayT, Object[].class);
|
||||
assertIsStrictSubtype(arrayT, Serializable.class);
|
||||
assertIsNot(arrayT, Serializable[].class);
|
||||
assertIsStrictSubtype(arrayT, Object.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAnAnnotationClass() {
|
||||
|
||||
ASTType arrayT =
|
||||
java.parse("class Foo { org.junit.Test field; }")
|
||||
.getFirstDescendantOfType(ASTType.class);
|
||||
|
||||
|
||||
assertIsExactlyA(arrayT, Test.class);
|
||||
assertIsStrictSubtype(arrayT, Annotation.class);
|
||||
assertIsStrictSubtype(arrayT, Object.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsAPrimitiveArrayClass() {
|
||||
|
||||
ASTType arrayT =
|
||||
java.parse("import java.io.ObjectStreamField; "
|
||||
+ "class Foo { private static final int[] serialPersistentFields; }")
|
||||
.getFirstDescendantOfType(ASTType.class);
|
||||
|
||||
|
||||
assertIsExactlyA(arrayT, int[].class);
|
||||
assertIsNot(arrayT, long[].class);
|
||||
assertIsNot(arrayT, Object[].class);
|
||||
|
||||
assertIsStrictSubtype(arrayT, Serializable.class);
|
||||
assertIsStrictSubtype(arrayT, Object.class);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testIsAFallbackAnnotation() {
|
||||
|
||||
@ -161,10 +211,38 @@ public class TypeTestUtilTest extends BaseNonParserTest {
|
||||
}
|
||||
|
||||
private void assertIsA(TypeNode node, Class<?> type) {
|
||||
Assert.assertTrue("TypeTestUtil::isA with class arg: " + type.getCanonicalName(),
|
||||
TypeTestUtil.isA(type, node));
|
||||
Assert.assertTrue("TypeTestUtil::isA with string arg: " + type.getCanonicalName(),
|
||||
TypeTestUtil.isA(type.getCanonicalName(), node));
|
||||
assertIsA(node, type, false, true);
|
||||
}
|
||||
|
||||
private void assertIsExactlyA(TypeNode node, Class<?> type) {
|
||||
assertIsA(node, type, true, true);
|
||||
assertIsA(node, type, false, true);
|
||||
}
|
||||
|
||||
private void assertIsNot(TypeNode node, Class<?> type) {
|
||||
assertIsA(node, type, true, false);
|
||||
assertIsA(node, type, false, false);
|
||||
}
|
||||
|
||||
private void assertIsNotExactly(TypeNode node, Class<?> type) {
|
||||
assertIsA(node, type, true, false);
|
||||
}
|
||||
|
||||
private void assertIsStrictSubtype(TypeNode node, Class<?> type) {
|
||||
assertIsNotExactly(node, type);
|
||||
assertIsA(node, type);
|
||||
}
|
||||
|
||||
private void assertIsA(TypeNode node, Class<?> type, boolean exactly, boolean expectTrue) {
|
||||
Assert.assertEquals("TypeTestUtil::isA with class arg: " + type.getCanonicalName(),
|
||||
expectTrue,
|
||||
exactly ? TypeTestUtil.isExactlyA(type, node)
|
||||
: TypeTestUtil.isA(type, node));
|
||||
Assert.assertEquals("TypeTestUtil::isA with string arg: " + type.getCanonicalName(),
|
||||
expectTrue,
|
||||
exactly ? TypeTestUtil.isExactlyA(type.getCanonicalName(), node)
|
||||
: TypeTestUtil.isA(type.getCanonicalName(), node));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
2
pom.xml
2
pom.xml
@ -95,7 +95,7 @@
|
||||
<checkstyle.version>8.30</checkstyle.version>
|
||||
<checkstyle.plugin.version>3.1.1</checkstyle.plugin.version>
|
||||
<pmd.plugin.version>3.13.0</pmd.plugin.version>
|
||||
<ant.version>1.10.1</ant.version>
|
||||
<ant.version>1.10.8</ant.version>
|
||||
<javadoc.plugin.version>3.2.0</javadoc.plugin.version>
|
||||
<antlr.version>4.7</antlr.version>
|
||||
|
||||
|
Reference in New Issue
Block a user