Better rationalize what to do with escaping objects
This commit is contained in:
2 files changed
+99
-46
No files matched your search
+55
-31
@@ -35,6 +35,8 @@ import net.sourceforge.pmd.lang.java.ast.ASTReturnStatement;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTThrowStatement;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTTypeExpression;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTVariableAccess;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclarator;
|
||||
import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId;
|
||||
import net.sourceforge.pmd.lang.java.ast.QualifiableExpression;
|
||||
import net.sourceforge.pmd.lang.java.ast.internal.PrettyPrintingUtil;
|
||||
import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule;
|
||||
@@ -45,6 +47,7 @@ import net.sourceforge.pmd.lang.java.rule.internal.DataflowPass.ReachingDefiniti
|
||||
import net.sourceforge.pmd.lang.java.symbols.JClassSymbol;
|
||||
import net.sourceforge.pmd.lang.java.symbols.JFieldSymbol;
|
||||
import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol;
|
||||
import net.sourceforge.pmd.lang.java.symbols.JVariableSymbol;
|
||||
import net.sourceforge.pmd.lang.java.types.JClassType;
|
||||
import net.sourceforge.pmd.lang.java.types.JTypeMirror;
|
||||
import net.sourceforge.pmd.lang.java.types.TypeOps;
|
||||
@@ -111,40 +114,64 @@ public class LawOfDemeterRule extends AbstractJavaRulechainRule {
|
||||
|
||||
@Override
|
||||
public Object visit(ASTFieldAccess node, Object data) {
|
||||
int degree = foreignDegree(node);
|
||||
if (isReportedDegree(degree)) {
|
||||
if (shouldReport(node)) {
|
||||
addViolationWithMessage(
|
||||
data, node,
|
||||
FIELD_ACCESS_ON_FOREIGN_VALUE,
|
||||
new Object[] {
|
||||
node.getName(),
|
||||
PrettyPrintingUtil.prettyPrint(node.getQualifier()),
|
||||
degree,
|
||||
}
|
||||
);
|
||||
foreignDegree(node.getQualifier()),
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object visit(ASTMethodCall node, Object data) {
|
||||
ASTExpression qualifier = node.getQualifier();
|
||||
if (qualifier != null) {
|
||||
int degree = foreignDegree(node);
|
||||
if (isReportedDegree(degree)) {
|
||||
addViolationWithMessage(
|
||||
data, node,
|
||||
METHOD_CALL_ON_FOREIGN_VALUE,
|
||||
new Object[] {
|
||||
node.getMethodName(),
|
||||
PrettyPrintingUtil.prettyPrint(node.getQualifier()),
|
||||
degree,
|
||||
});
|
||||
}
|
||||
if (shouldReport(node)) {
|
||||
addViolationWithMessage(
|
||||
data, node,
|
||||
METHOD_CALL_ON_FOREIGN_VALUE,
|
||||
new Object[] {
|
||||
node.getMethodName(),
|
||||
PrettyPrintingUtil.prettyPrint(node.getQualifier()),
|
||||
foreignDegree(node.getQualifier()),
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean shouldReport(QualifiableExpression expr) {
|
||||
ASTExpression qualifier = expr.getQualifier();
|
||||
if (qualifier == null) {
|
||||
return false;
|
||||
}
|
||||
int degree = foreignDegree(expr);
|
||||
if (isReportedDegree(degree) && isUsedAsGetter(expr)) {
|
||||
if (expr.getParent() instanceof ASTVariableDeclarator) { // NOPMD #3786
|
||||
// Stored in local var, don't report if some usages escape.
|
||||
// In that case, usage sites with non-escaping usage will be reported.
|
||||
return isAllowedStore(((ASTVariableDeclarator) expr.getParent()).getVarId());
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Reported degree may be higher if LHS is a local var with the reported degree.
|
||||
// If some usages of that local escape, the local hasn't been reported. Those usages
|
||||
// that don't escape need to be reported.
|
||||
if (qualifier instanceof ASTVariableAccess
|
||||
&& isReportedDegree(foreignDegree(qualifier))) {
|
||||
JVariableSymbol sym = ((ASTVariableAccess) qualifier).getReferencedSym();
|
||||
return sym != null && !isAllowedStore(sym.tryGetNode());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isAllowedStore(ASTVariableDeclaratorId varId) {
|
||||
return varId != null && varId.getLocalUsages().stream().noneMatch(this::escapesMethod);
|
||||
}
|
||||
|
||||
private int foreignDegree(@Nullable ASTExpression expr) {
|
||||
if (expr == null) {
|
||||
return 0;
|
||||
@@ -192,11 +219,11 @@ public class LawOfDemeterRule extends AbstractJavaRulechainRule {
|
||||
|| call.getQualifier() == null // either static or call on this. Prevents NPE when unresolved
|
||||
|| isFactoryMethod(call)
|
||||
|| isBuilderPattern(call.getQualifier())
|
||||
|| !isDangerousGetter(call)
|
||||
|| isPureData(call)) {
|
||||
return ACCESSIBLE;
|
||||
} else if (isPureDataContainer(call.getMethodType().getDeclaringType())
|
||||
|| isPureDataContainer(call.getTypeMirror())
|
||||
|| !isGetterCall(call)
|
||||
|| isTransformationMethod(call)) {
|
||||
return asForeignAsQualifier(call);
|
||||
}
|
||||
@@ -237,16 +264,15 @@ public class LawOfDemeterRule extends AbstractJavaRulechainRule {
|
||||
return false;
|
||||
}
|
||||
|
||||
// a dangerous getter is one that may be used later to call another getter
|
||||
private boolean isDangerousGetter(ASTMethodCall expr) {
|
||||
return isGetterCall(expr) && isUsedInThisMethod(expr);
|
||||
|
||||
private boolean escapesMethod(ASTExpression expr) {
|
||||
return expr.getParent() instanceof ASTArgumentList
|
||||
|| expr.getParent() instanceof ASTReturnStatement
|
||||
|| expr.getParent() instanceof ASTThrowStatement;
|
||||
}
|
||||
|
||||
private boolean isUsedInThisMethod(ASTExpression expr) {
|
||||
return !(expr.getParent() instanceof ASTExpressionStatement)
|
||||
&& !(expr.getParent() instanceof ASTArgumentList)
|
||||
&& !(expr.getParent() instanceof ASTReturnStatement)
|
||||
&& !(expr.getParent() instanceof ASTThrowStatement);
|
||||
private boolean isUsedAsGetter(ASTExpression expr) {
|
||||
return !escapesMethod(expr) && !(expr.getParent() instanceof ASTExpressionStatement);
|
||||
}
|
||||
|
||||
|
||||
@@ -266,9 +292,7 @@ public class LawOfDemeterRule extends AbstractJavaRulechainRule {
|
||||
}
|
||||
|
||||
private int fieldAccessDegree(ASTNamedReferenceExpr expr) {
|
||||
if (isRefToFieldOfThisClass(expr)
|
||||
|| isPureData(expr)
|
||||
|| !isUsedInThisMethod(expr)) {
|
||||
if (isRefToFieldOfThisClass(expr) || isPureData(expr)) {
|
||||
return ACCESSIBLE;
|
||||
} else if (isArrayLengthFieldAccess(expr)) {
|
||||
return asForeignAsQualifier((ASTFieldAccess) expr);
|
||||
@@ -344,7 +368,7 @@ public class LawOfDemeterRule extends AbstractJavaRulechainRule {
|
||||
* ctors, etc).
|
||||
* </ul>
|
||||
* You can use any method, but you can't use yourself the result of
|
||||
* a getter, or a field (though you can pass it as an argument to a method).
|
||||
* a getter, or field (though you can let it escape).
|
||||
*/
|
||||
private static final int ACCESSIBLE = 1;
|
||||
|
||||
|
||||
+44
-15
@@ -56,7 +56,7 @@ class Bar { C getC() {} }
|
||||
<description>Simple Method calls with chaining</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-messages>
|
||||
<message>Call to `getC` on foreign value `b` (degree 2)</message>
|
||||
<message>Call to `getC` on foreign value `b` (degree 1)</message>
|
||||
</expected-messages>
|
||||
<code><![CDATA[
|
||||
public class Foo {
|
||||
@@ -204,10 +204,10 @@ public class Foo {
|
||||
<expected-problems>4</expected-problems>
|
||||
<expected-linenumbers>18,19,24,25</expected-linenumbers>
|
||||
<expected-messages>
|
||||
<message>Access to field `a` on foreign value `b` (degree 2)</message>
|
||||
<message>Access to field `a` on foreign value `this.b` (degree 2)</message>
|
||||
<message>Access to field `a` on foreign value `b` (degree 2)</message>
|
||||
<message>Access to field `a` on foreign value `this.b` (degree 2)</message>
|
||||
<message>Access to field `a` on foreign value `b` (degree 1)</message>
|
||||
<message>Access to field `a` on foreign value `this.b` (degree 1)</message>
|
||||
<message>Access to field `a` on foreign value `b` (degree 1)</message>
|
||||
<message>Access to field `a` on foreign value `this.b` (degree 1)</message>
|
||||
</expected-messages>
|
||||
<code><![CDATA[
|
||||
public class B {
|
||||
@@ -414,7 +414,7 @@ class BarBuilder {
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>5</expected-linenumbers>
|
||||
<expected-messages>
|
||||
<message>Call to `getB` on foreign value `this.getA()` (degree 2)</message>
|
||||
<message>Call to `getB` on foreign value `this.getA()` (degree 1)</message>
|
||||
</expected-messages>
|
||||
<code><![CDATA[
|
||||
public class Test {
|
||||
@@ -438,7 +438,7 @@ interface B { A getA(); }
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>4</expected-linenumbers>
|
||||
<expected-messages>
|
||||
<message>Call to `getA` on foreign value `this.getA().getB()` (degree 3)</message>
|
||||
<message>Call to `getA` on foreign value `this.getA().getB()` (degree 2)</message>
|
||||
</expected-messages>
|
||||
<code><![CDATA[
|
||||
public class Test {
|
||||
@@ -680,8 +680,8 @@ public final class ControlEvent {
|
||||
<expected-problems>2</expected-problems>
|
||||
<expected-linenumbers>10,12</expected-linenumbers>
|
||||
<expected-messages>
|
||||
<message>Access to field `attributes` on foreign value `h` (degree 2)</message>
|
||||
<message>Access to field `attributes` on foreign value `h` (degree 2)</message>
|
||||
<message>Access to field `attributes` on foreign value `h` (degree 1)</message>
|
||||
<message>Access to field `attributes` on foreign value `h` (degree 1)</message>
|
||||
</expected-messages>
|
||||
<code><![CDATA[
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -760,7 +760,7 @@ public final class ControlEvent {
|
||||
<description>Field access on call</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-messages>
|
||||
<message>Access to field `manager` on foreign value `t.getLastToken()` (degree 2)</message>
|
||||
<message>Access to field `manager` on foreign value `t.getLastToken()` (degree 1)</message>
|
||||
</expected-messages>
|
||||
<code><![CDATA[
|
||||
import java.util.*;
|
||||
@@ -777,7 +777,8 @@ public final class ControlEvent {
|
||||
}
|
||||
class Q {
|
||||
void foo(Token t) {
|
||||
Manager checkMessage = t.getLastToken().manager;
|
||||
Manager m = t.getLastToken().manager;
|
||||
m.doSomethn();
|
||||
}
|
||||
}
|
||||
]]></code>
|
||||
@@ -963,7 +964,7 @@ public final class ControlEvent {
|
||||
<description>Foreign stream</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-messages>
|
||||
<message>Call to `getSomething` on foreign value `a` (degree 2)</message>
|
||||
<message>Call to `getSomething` on foreign value `a` (degree 1)</message>
|
||||
</expected-messages>
|
||||
<code><![CDATA[
|
||||
import java.util.*;
|
||||
@@ -1067,11 +1068,11 @@ public final class ControlEvent {
|
||||
]]></code>
|
||||
</test-code>
|
||||
<test-code>
|
||||
<description>Return and throw mean not used here</description>
|
||||
<description>Return and throw escape</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>3</expected-linenumbers>
|
||||
<expected-messages>
|
||||
<message>Call to `getLanguage` on foreign value `rule` (degree 2)</message>
|
||||
<message>Call to `getLanguage` on foreign value `rule` (degree 1)</message>
|
||||
</expected-messages>
|
||||
<code><![CDATA[
|
||||
class Foo {
|
||||
@@ -1079,7 +1080,7 @@ public final class ControlEvent {
|
||||
rule.getLanguage().doSomething(); // warn
|
||||
|
||||
println(rule.getLanguage()); // no warning
|
||||
if (foo)
|
||||
if (foo())
|
||||
return rule.getLanguage(); // no warning
|
||||
else
|
||||
throw rule.getLanguage(); // no warning
|
||||
@@ -1094,4 +1095,32 @@ public final class ControlEvent {
|
||||
}
|
||||
]]></code>
|
||||
</test-code>
|
||||
<test-code>
|
||||
<description>Local var is fine until used, if some of them escape</description>
|
||||
<expected-problems>1</expected-problems>
|
||||
<expected-linenumbers>4</expected-linenumbers>
|
||||
<expected-messages>
|
||||
<message>Call to `doSomething` on foreign value `l` (degree 2)</message>
|
||||
</expected-messages>
|
||||
<code><![CDATA[
|
||||
class Foo {
|
||||
public String toString(Rule rule) {
|
||||
Language l = rule.getLanguage(); // no warning
|
||||
l.doSomething(); // warn: degree 3
|
||||
println(l); // no warning
|
||||
if (foo())
|
||||
return l; // no warning
|
||||
else
|
||||
throw l; // no warning
|
||||
}
|
||||
}
|
||||
interface Language {
|
||||
String getName();
|
||||
void doSomething();
|
||||
}
|
||||
interface Rule {
|
||||
Language getLanguage();
|
||||
}
|
||||
]]></code>
|
||||
</test-code>
|
||||
</test-data>
|
||||
Reference in new issue
Block a user