diff --git a/.ci/files/all-java.xml b/.ci/files/all-java.xml index 365e5dc5c8..9a2ae34ab4 100644 --- a/.ci/files/all-java.xml +++ b/.ci/files/all-java.xml @@ -98,7 +98,7 @@ - + diff --git a/docs/pages/7_0_0_release_notes.md b/docs/pages/7_0_0_release_notes.md index ada38b10ba..e897367747 100644 --- a/docs/pages/7_0_0_release_notes.md +++ b/docs/pages/7_0_0_release_notes.md @@ -139,6 +139,8 @@ The following previously deprecated rules have been finally removed: * [#2883](https://github.com/pmd/pmd/issues/2883): \[java] JUnitAssertionsShouldIncludeMessage false positive with method call * [#2890](https://github.com/pmd/pmd/issues/2890): \[java] UnusedPrivateMethod false positive with generics * java-codestyle + * [#1208](https://github.com/pmd/pmd/issues/1208): \[java] PrematureDeclaration rule false-positive on variable declared to measure time + * [#1429](https://github.com/pmd/pmd/issues/1429): \[java] PrematureDeclaration as result of method call (false positive) * [#1673](https://github.com/pmd/pmd/issues/1673): \[java] UselessParentheses false positive with conditional operator * [#1790](https://github.com/pmd/pmd/issues/1790): \[java] UnnecessaryFullyQualifiedName false positive with enum constant * [#1918](https://github.com/pmd/pmd/issues/1918): \[java] UselessParentheses false positive with boolean operators @@ -146,6 +148,7 @@ The following previously deprecated rules have been finally removed: * [#2528](https://github.com/pmd/pmd/issues/2528): \[java] MethodNamingConventions - JUnit 5 method naming not support ParameterizedTest * [#2739](https://github.com/pmd/pmd/issues/2739): \[java] UselessParentheses false positive for string concatenation * [#3195](https://github.com/pmd/pmd/pull/3195): \[java] Improve rule UnnecessaryReturn to detect more cases + * [#3221](https://github.com/pmd/pmd/issues/3221): \[java] PrematureDeclaration false positive for unused variables * java-errorprone * [#1005](https://github.com/pmd/pmd/issues/1005): \[java] CloneMethodMustImplementCloneable triggers for interfaces * [#2532](https://github.com/pmd/pmd/issues/2532): \[java] AvoidDecimalLiteralsInBigDecimalConstructor can not detect the case new BigDecimal(Expression) diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/ForLoopCanBeForeachRule.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/ForLoopCanBeForeachRule.java index d8999bf264..8b0beaeac1 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/ForLoopCanBeForeachRule.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/ForLoopCanBeForeachRule.java @@ -32,8 +32,8 @@ import net.sourceforge.pmd.lang.java.ast.BinaryOp; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule; import net.sourceforge.pmd.lang.java.rule.internal.JavaRuleUtil; import net.sourceforge.pmd.lang.java.symbols.JVariableSymbol; +import net.sourceforge.pmd.lang.java.types.InvocationMatcher; import net.sourceforge.pmd.lang.java.types.TypeTestUtil; -import net.sourceforge.pmd.lang.java.types.TypeTestUtil.InvocationMatcher; /** * @author Clément Fournier diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/JUnitAssertionsShouldIncludeMessageRule.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/JUnitAssertionsShouldIncludeMessageRule.java index 1148593bb0..fcccd4db6d 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/JUnitAssertionsShouldIncludeMessageRule.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices/JUnitAssertionsShouldIncludeMessageRule.java @@ -4,31 +4,28 @@ package net.sourceforge.pmd.lang.java.rule.bestpractices; -import static net.sourceforge.pmd.util.CollectionUtil.listOf; - -import java.util.List; - import net.sourceforge.pmd.lang.java.ast.ASTMethodCall; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule; import net.sourceforge.pmd.lang.java.rule.internal.TestFrameworksUtil; -import net.sourceforge.pmd.lang.java.types.TypeTestUtil.InvocationMatcher; +import net.sourceforge.pmd.lang.java.types.InvocationMatcher; +import net.sourceforge.pmd.lang.java.types.InvocationMatcher.CompoundInvocationMatcher; public class JUnitAssertionsShouldIncludeMessageRule extends AbstractJavaRulechainRule { - private final List checks = - listOf( - InvocationMatcher.parse("_#assertEquals(_,_)"), - InvocationMatcher.parse("_#assertTrue(_)"), - InvocationMatcher.parse("_#assertFalse(_)"), - InvocationMatcher.parse("_#assertSame(_,_)"), - InvocationMatcher.parse("_#assertNotSame(_,_)"), - InvocationMatcher.parse("_#assertNull(_)"), - InvocationMatcher.parse("_#assertNotNull(_)"), - InvocationMatcher.parse("_#assertArrayEquals(_,_)"), - InvocationMatcher.parse("_#assertThat(_,_)"), - InvocationMatcher.parse("_#fail()"), - InvocationMatcher.parse("_#assertEquals(float,float,float)"), - InvocationMatcher.parse("_#assertEquals(double,double,double)") + private final CompoundInvocationMatcher checks = + InvocationMatcher.parseAll( + "_#assertEquals(_,_)", + "_#assertTrue(_)", + "_#assertFalse(_)", + "_#assertSame(_,_)", + "_#assertNotSame(_,_)", + "_#assertNull(_)", + "_#assertNotNull(_)", + "_#assertArrayEquals(_,_)", + "_#assertThat(_,_)", + "_#fail()", + "_#assertEquals(float,float,float)", + "_#assertEquals(double,double,double)" ); public JUnitAssertionsShouldIncludeMessageRule() { @@ -38,11 +35,8 @@ public class JUnitAssertionsShouldIncludeMessageRule extends AbstractJavaRulecha @Override public Object visit(ASTMethodCall node, Object data) { if (TestFrameworksUtil.isCallOnAssertionContainer(node)) { - for (InvocationMatcher check : checks) { - if (check.matchesCall(node)) { - addViolation(data, node); - break; - } + if (checks.anyMatch(node)) { + addViolation(data, node); } } return null; diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/PrematureDeclarationRule.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/PrematureDeclarationRule.java index c591f96449..edc05e1d4c 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/PrematureDeclarationRule.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/codestyle/PrematureDeclarationRule.java @@ -4,20 +4,28 @@ package net.sourceforge.pmd.lang.java.rule.codestyle; +import static java.util.Collections.emptySet; import static net.sourceforge.pmd.lang.ast.NodeStream.asInstanceOf; -import java.util.ArrayList; -import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; -import net.sourceforge.pmd.lang.ast.Node; -import net.sourceforge.pmd.lang.java.ast.ASTBlockStatement; +import net.sourceforge.pmd.lang.ast.NodeStream; +import net.sourceforge.pmd.lang.java.ast.ASTAssignableExpr.ASTNamedReferenceExpr; +import net.sourceforge.pmd.lang.java.ast.ASTExpression; import net.sourceforge.pmd.lang.java.ast.ASTForInit; import net.sourceforge.pmd.lang.java.ast.ASTLocalVariableDeclaration; -import net.sourceforge.pmd.lang.java.ast.ASTName; +import net.sourceforge.pmd.lang.java.ast.ASTResource; import net.sourceforge.pmd.lang.java.ast.ASTReturnStatement; +import net.sourceforge.pmd.lang.java.ast.ASTStatement; import net.sourceforge.pmd.lang.java.ast.ASTThrowStatement; +import net.sourceforge.pmd.lang.java.ast.ASTVariableAccess; import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId; -import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; +import net.sourceforge.pmd.lang.java.rule.AbstractJavaRulechainRule; +import net.sourceforge.pmd.lang.java.rule.internal.JavaRuleUtil; +import net.sourceforge.pmd.lang.java.symbols.JVariableSymbol; +import net.sourceforge.pmd.lang.java.types.InvocationMatcher; +import net.sourceforge.pmd.lang.java.types.InvocationMatcher.CompoundInvocationMatcher; /** * Checks for variables in methods that are defined before they are really @@ -27,90 +35,98 @@ import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; * * @author Brian Remedios */ -public class PrematureDeclarationRule extends AbstractJavaRule { +public class PrematureDeclarationRule extends AbstractJavaRulechainRule { + private static final CompoundInvocationMatcher TIME_METHODS = + InvocationMatcher.parseAll( + "java.lang.System#nanoTime()", + "java.lang.System#currentTimeMillis()" + ); + + public PrematureDeclarationRule() { + super(ASTLocalVariableDeclaration.class); + } @Override public Object visit(ASTLocalVariableDeclaration node, Object data) { - - // is it part of a for-loop declaration? - if (node.getParent() instanceof ASTForInit) { - // yes, those don't count - return super.visit(node, data); + if (node.getParent() instanceof ASTForInit + || node.getParent() instanceof ASTResource) { + // those don't count + return null; } for (ASTVariableDeclaratorId id : node) { - for (ASTBlockStatement block : statementsAfter(node)) { - if (hasReferencesIn(block, id.getVariableName())) { + ASTExpression initializer = id.getInitializer(); + + if (JavaRuleUtil.isNeverUsed(id) // avoid the duplicate with unused variables + || cannotBeMoved(initializer) + || JavaRuleUtil.hasSideEffect(initializer, emptySet())) { + continue; + } + + Set refsInInitializer = getReferencedVars(initializer); + // If there's no initializer, or the initializer doesn't depend on anything (eg, a literal), + // then we don't care about side-effects + boolean hasStatefulInitializer = !refsInInitializer.isEmpty() || JavaRuleUtil.hasSideEffect(initializer, emptySet()); + for (ASTStatement stmt : statementsAfter(node)) { + if (hasReferencesIn(stmt, id) + || hasStatefulInitializer && JavaRuleUtil.hasSideEffect(stmt, refsInInitializer)) { break; } - if (hasExit(block)) { - addViolation(data, node); + if (hasExit(stmt)) { + addViolation(data, node, id.getName()); break; } } } - - return super.visit(node, data); + return null; } + /** + * Returns the set of local variables referenced inside the expression. + */ + private static Set getReferencedVars(ASTExpression term) { + return term == null ? emptySet() + : term.descendantsOrSelf() + .filterIs(ASTNamedReferenceExpr.class) + .filter(it -> it.getReferencedSym() != null) + .collect(Collectors.mapping(ASTNamedReferenceExpr::getReferencedSym, Collectors.toSet())); + } + + /** + * Time methods cannot be moved ever, even when there are no side-effects. + * The side effect they depend on is the program being executed. Are they + * the only methods like that? + */ + private boolean cannotBeMoved(ASTExpression initializer) { + return TIME_METHODS.anyMatch(initializer); + } /** * Returns whether the block contains a return call or throws an exception. * Exclude blocks that have these things as part of an inner class. */ - private boolean hasExit(ASTBlockStatement block) { - return block.descendants().map(asInstanceOf(ASTThrowStatement.class, ASTReturnStatement.class)).nonEmpty(); + private static boolean hasExit(ASTStatement block) { + return block.descendants() + .map(asInstanceOf(ASTThrowStatement.class, ASTReturnStatement.class)) + .nonEmpty(); } /** * Returns whether the variable is mentioned within the statement or not. */ - private static boolean hasReferencesIn(ASTBlockStatement block, String varName) { - - // allow for closures on the var - for (ASTName name : block.findDescendantsOfType(ASTName.class, true)) { - if (isReference(varName, name.getImage())) { - return true; - } - } - return false; + private static boolean hasReferencesIn(ASTStatement stmt, ASTVariableDeclaratorId var) { + return stmt.descendants(ASTVariableAccess.class) + .crossFindBoundaries() + .filterMatching(ASTNamedReferenceExpr::getReferencedSym, var.getSymbol()) + .nonEmpty(); } - - /** - * Return whether the shortName is part of the compound name by itself or as - * a method call receiver. - */ - private static boolean isReference(String shortName, String compoundName) { - int dotPos = compoundName.indexOf('.'); - - return dotPos < 0 ? shortName.equals(compoundName) : shortName.equals(compoundName.substring(0, dotPos)); - } - - - /** - * Returns all the block statements following the given local var declaration. - */ - private static List statementsAfter(ASTLocalVariableDeclaration node) { - - Node blockOrSwitch = node.getParent().getParent(); - - int count = blockOrSwitch.getNumChildren(); - int start = node.getParent().getIndexInParent() + 1; - - List nextBlocks = new ArrayList<>(count - start); - - for (int i = start; i < count; i++) { - Node maybeBlock = blockOrSwitch.getChild(i); - if (maybeBlock instanceof ASTBlockStatement) { - nextBlocks.add((ASTBlockStatement) maybeBlock); - } - } - - return nextBlocks; + /** Returns all the statements following the given local var declaration. */ + private static NodeStream statementsAfter(ASTLocalVariableDeclaration node) { + return node.asStream().followingSiblings().filterIs(ASTStatement.class); } } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/internal/JavaRuleUtil.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/internal/JavaRuleUtil.java index 260d94a6b2..ea346cbbb8 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/internal/JavaRuleUtil.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/internal/JavaRuleUtil.java @@ -28,6 +28,8 @@ import net.sourceforge.pmd.lang.ast.NodeStream; import net.sourceforge.pmd.lang.ast.impl.javacc.JavaccToken; import net.sourceforge.pmd.lang.java.ast.ASTAnyTypeDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTArgumentList; +import net.sourceforge.pmd.lang.java.ast.ASTArrayAccess; +import net.sourceforge.pmd.lang.java.ast.ASTAssignableExpr; import net.sourceforge.pmd.lang.java.ast.ASTAssignableExpr.ASTNamedReferenceExpr; import net.sourceforge.pmd.lang.java.ast.ASTAssignableExpr.AccessType; import net.sourceforge.pmd.lang.java.ast.ASTAssignmentExpression; @@ -55,6 +57,7 @@ import net.sourceforge.pmd.lang.java.ast.ASTNumericLiteral; import net.sourceforge.pmd.lang.java.ast.ASTStatement; import net.sourceforge.pmd.lang.java.ast.ASTSuperExpression; import net.sourceforge.pmd.lang.java.ast.ASTThisExpression; +import net.sourceforge.pmd.lang.java.ast.ASTThrowStatement; import net.sourceforge.pmd.lang.java.ast.ASTUnaryExpression; import net.sourceforge.pmd.lang.java.ast.ASTVariableAccess; import net.sourceforge.pmd.lang.java.ast.ASTVariableDeclaratorId; @@ -68,6 +71,8 @@ import net.sourceforge.pmd.lang.java.ast.TypeNode; import net.sourceforge.pmd.lang.java.ast.UnaryOp; import net.sourceforge.pmd.lang.java.symbols.JFieldSymbol; import net.sourceforge.pmd.lang.java.symbols.JVariableSymbol; +import net.sourceforge.pmd.lang.java.types.InvocationMatcher; +import net.sourceforge.pmd.lang.java.types.InvocationMatcher.CompoundInvocationMatcher; import net.sourceforge.pmd.lang.java.types.JPrimitiveType.PrimitiveTypeKind; import net.sourceforge.pmd.lang.java.types.JTypeMirror; import net.sourceforge.pmd.lang.java.types.TypeTestUtil; @@ -78,6 +83,22 @@ import net.sourceforge.pmd.util.CollectionUtil; */ public final class JavaRuleUtil { + // this is a hacky way to do it, but let's see where this goes + private static final CompoundInvocationMatcher KNOWN_PURE_METHODS = InvocationMatcher.parseAll( + "_#toString()", + "_#hashCode()", + "_#equals(java.lang.Object)", + "java.lang.String#_(_*)", + // actually not all of them, probs only stream of some type + // arg which doesn't implement Closeable... + "java.util.stream.Stream#_(_*)", + "java.util.Collection#size()", + "java.util.List#get(int)", + "java.util.Map#get(_)", + "java.lang.Iterable#iterator()", + "java.lang.Comparable#compareTo(_)" + ); + private JavaRuleUtil() { // utility class } @@ -648,6 +669,17 @@ public final class JavaRuleUtil { return e instanceof ASTThisExpression && ((ASTThisExpression) e).getQualifier() == null; } + /** + * Returns true if the expression is a {@link ASTNamedReferenceExpr} + * that references any of the symbol in the set. + */ + public static boolean isReferenceToVar(@Nullable ASTExpression expression, @NonNull Set symbols) { + if (expression instanceof ASTNamedReferenceExpr) { + return symbols.contains(((ASTNamedReferenceExpr) expression).getReferencedSym()); + } + return false; + } + /** * Returns true if both expressions refer to the same variable. * A "variable" here can also means a field path, eg, {@code this.field.a}. @@ -688,6 +720,17 @@ public final class JavaRuleUtil { return false; } + /** + * Returns true if the expression is a reference to a local variable. + */ + public static boolean isReferenceToLocal(ASTExpression expr) { + if (expr instanceof ASTVariableAccess) { + JVariableSymbol sym = ((ASTVariableAccess) expr).getReferencedSym(); + return sym != null && !sym.isField(); + } + return false; + } + /** * Returns true if the expression has the form `field`, or `this.field`, * where `field` is a field declared in the enclosing class. @@ -750,4 +793,59 @@ public final class JavaRuleUtil { Node parent = it.getParent(); return parent == null || it.getIndexInParent() == parent.getNumChildren() - 1; } + + + /** + * Whether the node or one of its descendants is an expression with + * side effects. Conservatively, any method call is a potential side-effect, + * as well as assignments to fields or array elements. We could relax + * this assumption with (much) more data-flow logic, including a memory model. + * + *

By default assignments to locals are not counted as side-effects, + * unless the lhs is in the given set of symbols. + * + * @param node A node + * @param localVarsToTrack Local variables to track + */ + public static boolean hasSideEffect(@Nullable JavaNode node, Set localVarsToTrack) { + return node != null && node.descendantsOrSelf() + .filterIs(ASTExpression.class) + .any(e -> hasSideEffectNonRecursive(e, localVarsToTrack)); + } + + /** + * Returns true if the expression has side effects we don't track. + * Does not recurse into sub-expressions. + */ + private static boolean hasSideEffectNonRecursive(ASTExpression e, Set localVarsToTrack) { + if (e instanceof ASTAssignmentExpression) { + ASTAssignableExpr lhs = ((ASTAssignmentExpression) e).getLeftOperand(); + return isNonLocalLhs(lhs) || isReferenceToVar(lhs, localVarsToTrack); + } else if (e instanceof ASTUnaryExpression) { + ASTUnaryExpression unary = (ASTUnaryExpression) e; + ASTExpression lhs = unary.getOperand(); + return !unary.getOperator().isPure() + && (isNonLocalLhs(lhs) || isReferenceToVar(lhs, localVarsToTrack)); + } + + if (e.ancestors(ASTThrowStatement.class).nonEmpty()) { + // then this side effect can never be observed in containing code, + // because control flow jumps out of the method + return false; + } + + return e instanceof ASTMethodCall && !isPure((ASTMethodCall) e) + || e instanceof ASTConstructorCall; + } + + private static boolean isNonLocalLhs(ASTExpression lhs) { + return lhs instanceof ASTArrayAccess || !isReferenceToLocal(lhs); + } + + /** + * Whether the invocation has no side-effects. Very conservative. + */ + private static boolean isPure(ASTMethodCall call) { + return isGetterCall(call) || KNOWN_PURE_METHODS.anyMatch(call); + } } diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/xpath/internal/MatchesSignatureFunction.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/xpath/internal/MatchesSignatureFunction.java index a2a27f0224..187d100612 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/xpath/internal/MatchesSignatureFunction.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/xpath/internal/MatchesSignatureFunction.java @@ -5,7 +5,7 @@ package net.sourceforge.pmd.lang.java.rule.xpath.internal; import net.sourceforge.pmd.lang.java.ast.InvocationNode; -import net.sourceforge.pmd.lang.java.types.TypeTestUtil.InvocationMatcher; +import net.sourceforge.pmd.lang.java.types.InvocationMatcher; import net.sf.saxon.trans.XPathException; diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/InvocationMatcher.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/InvocationMatcher.java new file mode 100644 index 0000000000..0c52fe4b15 --- /dev/null +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/InvocationMatcher.java @@ -0,0 +1,334 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.java.types; + +import static net.sourceforge.pmd.util.CollectionUtil.listOf; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.checkerframework.checker.nullness.qual.Nullable; + +import net.sourceforge.pmd.internal.util.AssertionUtil; +import net.sourceforge.pmd.lang.java.ast.ASTConstructorCall; +import net.sourceforge.pmd.lang.java.ast.ASTExpression; +import net.sourceforge.pmd.lang.java.ast.ASTList; +import net.sourceforge.pmd.lang.java.ast.InvocationNode; +import net.sourceforge.pmd.lang.java.ast.JavaNode; +import net.sourceforge.pmd.lang.java.ast.QualifiableExpression; +import net.sourceforge.pmd.util.CollectionUtil; +import net.sourceforge.pmd.util.OptionalBool; +import net.sourceforge.pmd.util.StringUtil; + +/** + * Matches a method or constructor call against a particular overload. + * Use {@link #parse(String)} to create one. For example, + * + *

+ *     java.lang.String#toString()   // match calls to toString on String instances
+ *     _#toString()                  // match calls to toString on any receiver
+ *     _#_()                         // match all calls to a method with no parameters
+ *     _#toString(_*)                // match calls to a "toString" method with any number of parameters
+ *     _#eq(_, _)                    // match calls to an "eq" method that has 2 parameters of unspecified type
+ *     _#eq(java.lang.String, _)     // like the previous, but the first parameter must be String
+ *     java.util.ArrayList#new(int)  // match constructor calls of this overload of the ArrayList constructor
+ * 
+ * + *

The receiver matcher (first half) is matched against the + * static type of the receiver of the call, and not the + * declaration site of the method, unless the called method is + * static, or a constructor. + * + *

The parameters are matched against the declared parameters + * types of the called overload, and not the actual argument types. + * In particular, for vararg methods, the signature should mention + * a single parameter, with an array type. + * + *

For example {@code Integer.valueOf('0')} will be matched by + * {@code _#valueOf(int)} but not {@code _#valueOf(char)}, which is + * an overload that does not exist (the char is widened to an int, + * so the int overload is selected). + * + *

Full EBNF grammar
+ * + *

(no whitespace is tolerated anywhere): + *

{@code
+ * sig         ::= type '#' method_name param_list
+ * type        ::= qname ( '[]' )* | '_'
+ * method_name ::= '_' | ident | 'new'
+ * param_list  ::= '(_*)' | '(' type (',' type )* ')'
+ * qname       ::= java binary name
+ * }
+ */ +public final class InvocationMatcher { + + final @Nullable String expectedName; + final @Nullable List argMatchers; + final TypeMatcher qualifierMatcher; + + InvocationMatcher(TypeMatcher qualifierMatcher, String expectedName, @Nullable List argMatchers) { + this.expectedName = "_".equals(expectedName) ? null : expectedName; + this.argMatchers = argMatchers; + this.qualifierMatcher = qualifierMatcher; + } + + /** + * See {@link #matchesCall(InvocationNode)}. + */ + public boolean matchesCall(@Nullable JavaNode node) { + if (node instanceof InvocationNode) { + return matchesCall((InvocationNode) node); + } + return false; + } + + /** + * Returns true if the call matches this matcher. This means, + * the called overload is the one identified by the argument + * matchers, and the actual qualifier type is a subtype of the + * one mentioned by the qualifier matcher. + */ + public boolean matchesCall(@Nullable InvocationNode node) { + if (node == null) { + return false; + } + if (expectedName != null && !node.getMethodName().equals(expectedName) + || argMatchers != null && ASTList.sizeOrZero(node.getArguments()) != argMatchers.size()) { + return false; + } + OverloadSelectionResult info = node.getOverloadSelectionInfo(); + if (info.isFailed() || !matchQualifier(node)) { + return false; + } + return argsMatchOverload(info.getMethodType()); + } + + private boolean matchQualifier(InvocationNode node) { + if (qualifierMatcher == TypeMatcher.ANY) { // NOPMD CompareObjectsWithEquals + return true; + } + if (node instanceof ASTConstructorCall) { + JTypeMirror newType = ((ASTConstructorCall) node).getTypeNode().getTypeMirror(); + return qualifierMatcher.matches(newType, true); + } + JMethodSig m = node.getMethodType(); + JTypeMirror qualType; + if (node instanceof QualifiableExpression) { + ASTExpression qualifier = ((QualifiableExpression) node).getQualifier(); + if (qualifier != null) { + qualType = qualifier.getTypeMirror(); + } else { + // todo: if qualifier == null, then we should take the type of the + // implicit receiver, ie `this` or `SomeOuter.this` + qualType = m.getDeclaringType(); + } + } else { + qualType = m.getDeclaringType(); + } + + return qualifierMatcher.matches(qualType, m.isStatic()); + } + + private boolean argsMatchOverload(JMethodSig invoc) { + if (argMatchers == null) { + return true; + } + List formals = invoc.getFormalParameters(); + if (invoc.getArity() != argMatchers.size()) { + return false; + } + for (int i = 0; i < formals.size(); i++) { + if (!argMatchers.get(i).matches(formals.get(i), true)) { + return false; + } + } + return true; + } + + + /** + * Parses a {@link CompoundInvocationMatcher} which matches any of + * the provided matchers. + * + * @param first First signature, in the format described on this class + * @param rest Other signatures, in the format described on this class + * + * @return A sig matcher + * + * @throws IllegalArgumentException If any signature is malformed (see EBNF) + * @throws NullPointerException If any signature is null + * @see #parse(String) + */ + public static CompoundInvocationMatcher parseAll(String first, String... rest) { + List matchers = CollectionUtil.map(listOf(first, rest), InvocationMatcher::parse); + return new CompoundInvocationMatcher(matchers); + } + + /** + * Parses an {@link InvocationMatcher}. + * + * @param sig A signature in the format described on this class + * + * @return A sig matcher + * + * @throws IllegalArgumentException If the signature is malformed (see EBNF) + * @throws NullPointerException If the signature is null + * @see #parseAll(String, String...) + */ + public static InvocationMatcher parse(String sig) { + int i = parseType(sig, 0); + final TypeMatcher qualifierMatcher = newMatcher(sig.substring(0, i)); + i = consumeChar(sig, i, '#'); + final int nameStart = i; + i = parseSimpleName(sig, i); + final String methodName = sig.substring(nameStart, i); + i = consumeChar(sig, i, '('); + if (isChar(sig, i, ')')) { + return new InvocationMatcher(qualifierMatcher, methodName, Collections.emptyList()); + } + // (_*) matches any argument list + List argMatchers; + if (isChar(sig, i, '_') + && isChar(sig, i + 1, '*') + && isChar(sig, i + 2, ')')) { + argMatchers = null; + i = i + 3; + } else { + argMatchers = new ArrayList<>(); + i = parseArgList(sig, i, argMatchers); + } + if (i != sig.length()) { + throw new IllegalArgumentException("Not a valid signature " + sig); + } + return new InvocationMatcher(qualifierMatcher, methodName, argMatchers); + } + + private static int parseSimpleName(String sig, final int start) { + int i = start; + while (i < sig.length() && Character.isJavaIdentifierPart(sig.charAt(i))) { + i++; + } + if (i == start) { + throw new IllegalArgumentException("Not a valid signature " + sig); + } + return i; + } + + private static int parseArgList(String sig, int i, List argMatchers) { + while (i < sig.length()) { + i = parseType(sig, i, argMatchers); + if (isChar(sig, i, ')')) { + return i + 1; + } + i = consumeChar(sig, i, ','); + } + throw new IllegalArgumentException("Not a valid signature " + sig); + } + + private static int consumeChar(String source, int i, char c) { + if (isChar(source, i, c)) { + return i + 1; + } + throw newParseException(source, i, "character '" + c + "'"); + } + + private static RuntimeException newParseException(String source, int i, String expectedWhat) { + final String indent = " "; + String message = "Expected " + expectedWhat + " at index " + i + ":\n"; + message += indent + "\"" + StringUtil.escapeJava(source) + "\"\n"; + message += indent + StringUtils.repeat(' ', i + 1) + '^' + "\n"; + return new IllegalArgumentException(message); + } + + private static boolean isChar(String source, int i, char c) { + return i < source.length() && source.charAt(i) == c; + } + + private static int parseType(String source, int i, List result) { + final int start = i; + i = parseType(source, i); + result.add(newMatcher(source.substring(start, i))); + return i; + } + + private static int parseType(String source, int i) { + final int start = i; + while (i < source.length() && (Character.isJavaIdentifierPart(source.charAt(i)) + || source.charAt(i) == '.')) { + i++; + } + if (i == start) { + throw newParseException(source, i, "type"); + } + + AssertionUtil.assertValidJavaBinaryName(source.substring(start, i)); + // array dimensions + while (isChar(source, i, '[')) { + i = consumeChar(source, i + 1, ']'); + } + return i; + } + + private static TypeMatcher newMatcher(String name) { + return "_".equals(name) ? TypeMatcher.ANY : new TypeMatcher(name); + } + + private static final class TypeMatcher { + + /** Matches any type. */ + public static final TypeMatcher ANY = new TypeMatcher(null); + + final @Nullable String name; + + private TypeMatcher(@Nullable String name) { + this.name = name; + } + + boolean matches(JTypeMirror type, boolean exact) { + if (name == null) { + return true; + } + return exact ? TypeTestUtil.isExactlyAOrAnon(name, type) == OptionalBool.YES + : TypeTestUtil.isA(name, type); + } + } + + /** + * A compound of several matchers (logical OR). Get one from + * {@link InvocationMatcher#parseAll(String, String...)}; + */ + public static final class CompoundInvocationMatcher { + + private final List matchers; + + private CompoundInvocationMatcher(List matchers) { + this.matchers = matchers; + } + + // todo make this smarter. Like collecting all possible names + // into a set to do a quick pre-screening before we test + // everything linearly + + /** + * Returns true if any of the matchers match the node. + * + * @see #matchesCall(JavaNode) + */ + public boolean anyMatch(InvocationNode node) { + return CollectionUtil.any(matchers, it -> it.matchesCall(node)); + } + + /** + * Returns true if any of the matchers match the node. + * + * @see #matchesCall(JavaNode) + */ + public boolean anyMatch(JavaNode node) { + return CollectionUtil.any(matchers, it -> it.matchesCall(node)); + } + } +} diff --git a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeTestUtil.java b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeTestUtil.java index e33284a5d5..b7b243bc62 100644 --- a/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeTestUtil.java +++ b/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/types/TypeTestUtil.java @@ -5,32 +5,24 @@ package net.sourceforge.pmd.lang.java.types; import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import org.apache.commons.lang3.StringUtils; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import net.sourceforge.pmd.internal.util.AssertionUtil; -import net.sourceforge.pmd.lang.java.ast.ASTConstructorCall; -import net.sourceforge.pmd.lang.java.ast.ASTExpression; -import net.sourceforge.pmd.lang.java.ast.ASTList; import net.sourceforge.pmd.lang.java.ast.InternalApiBridge; -import net.sourceforge.pmd.lang.java.ast.InvocationNode; -import net.sourceforge.pmd.lang.java.ast.JavaNode; -import net.sourceforge.pmd.lang.java.ast.QualifiableExpression; import net.sourceforge.pmd.lang.java.ast.TypeNode; import net.sourceforge.pmd.lang.java.symbols.JClassSymbol; import net.sourceforge.pmd.lang.java.symbols.JTypeDeclSymbol; import net.sourceforge.pmd.lang.java.symbols.JTypeParameterSymbol; import net.sourceforge.pmd.lang.java.symbols.internal.UnresolvedClassStore; import net.sourceforge.pmd.util.OptionalBool; -import net.sourceforge.pmd.util.StringUtil; /** * Public utilities to test the type of nodes. + * + * @see InvocationMatcher */ public final class TypeTestUtil { @@ -269,7 +261,7 @@ public final class TypeTestUtil { return isExactlyAOrAnon(canonicalName, node.getTypeMirror()) == OptionalBool.YES; } - private static OptionalBool isExactlyAOrAnon(@NonNull String canonicalName, final @NonNull JTypeMirror node) { + static OptionalBool isExactlyAOrAnon(@NonNull String canonicalName, final @NonNull JTypeMirror node) { AssertionUtil.requireParamNotNull("canonicalName", canonicalName); JTypeDeclSymbol sym = node.getSymbol(); @@ -296,259 +288,4 @@ public final class TypeTestUtil { } - /** - * Matches a method or constructor call against a particular overload. - * Use {@link #parse(String)} to create one. For example, - * - *
-     *     java.lang.String#toString()   // match calls to toString on String instances
-     *     _#toString()                  // match calls to toString on any receiver
-     *     _#_()                         // match all calls to a method with no parameters
-     *     _#toString(_*)                // match calls to a "toString" method with any number of parameters
-     *     _#eq(_, _)                    // match calls to an "eq" method that has 2 parameters of unspecified type
-     *     _#eq(java.lang.String, _)     // like the previous, but the first parameter must be String
-     *     java.util.ArrayList#new(int)  // match constructor calls of this overload of the ArrayList constructor
-     * 
- * - *

The receiver matcher (first half) is matched against the - * static type of the receiver of the call, and not the - * declaration site of the method, unless the called method is - * static, or a constructor. - * - *

The parameters are matched against the declared parameters - * types of the called overload, and not the actual argument types. - * In particular, for vararg methods, the signature should mention - * a single parameter, with an array type. - * - *

For example {@code Integer.valueOf('0')} will be matched by - * {@code _#valueOf(int)} but not {@code _#valueOf(char)}, which is - * an overload that does not exist (the char is widened to an int, - * so the int overload is selected). - * - *

Full EBNF grammar
- * - *

(no whitespace is tolerated anywhere): - *

{@code
-     * sig         ::= type '#' method_name param_list
-     * type        ::= qname ( '[]' )* | '_'
-     * method_name ::= '_' | ident | 'new'
-     * param_list  ::= '(_*)' | '(' type (',' type )* ')'
-     * qname       ::= java binary name
-     * }
- */ - public static final class InvocationMatcher { - - final @Nullable String expectedName; - final @Nullable List argMatchers; - final TypeMatcher qualifierMatcher; - - InvocationMatcher(TypeMatcher qualifierMatcher, String expectedName, @Nullable List argMatchers) { - this.expectedName = "_".equals(expectedName) ? null : expectedName; - this.argMatchers = argMatchers; - this.qualifierMatcher = qualifierMatcher; - } - - /** - * See {@link #matchesCall(InvocationNode)}. - */ - public boolean matchesCall(@Nullable JavaNode node) { - if (node instanceof InvocationNode) { - return matchesCall((InvocationNode) node); - } - return false; - } - - /** - * Returns true if the call matches this matcher. This means, - * the called overload is the one identified by the argument - * matchers, and the actual qualifier type is a subtype of the - * one mentioned by the qualifier matcher. - */ - public boolean matchesCall(@Nullable InvocationNode node) { - if (node == null) { - return false; - } - if (expectedName != null && !node.getMethodName().equals(expectedName) - || argMatchers != null && ASTList.sizeOrZero(node.getArguments()) != argMatchers.size()) { - return false; - } - OverloadSelectionResult info = node.getOverloadSelectionInfo(); - if (info.isFailed() || !matchQualifier(node)) { - return false; - } - return argsMatchOverload(info.getMethodType()); - } - - private boolean matchQualifier(InvocationNode node) { - if (qualifierMatcher == TypeMatcher.ANY) { - return true; - } - if (node instanceof ASTConstructorCall) { - JTypeMirror newType = ((ASTConstructorCall) node).getTypeNode().getTypeMirror(); - return qualifierMatcher.matches(newType, true); - } - JMethodSig m = node.getMethodType(); - JTypeMirror qualType; - if (node instanceof QualifiableExpression) { - ASTExpression qualifier = ((QualifiableExpression) node).getQualifier(); - if (qualifier != null) { - qualType = qualifier.getTypeMirror(); - } else { - // todo: if qualifier == null, then we should take the type of the - // implicit receiver, ie `this` or `SomeOuter.this` - qualType = m.getDeclaringType(); - } - } else { - qualType = m.getDeclaringType(); - } - - return qualifierMatcher.matches(qualType, m.isStatic()); - } - - private boolean argsMatchOverload(JMethodSig invoc) { - if (argMatchers == null) { - return true; - } - List formals = invoc.getFormalParameters(); - if (invoc.getArity() != argMatchers.size()) { - return false; - } - for (int i = 0; i < formals.size(); i++) { - if (!argMatchers.get(i).matches(formals.get(i), true)) { - return false; - } - } - return true; - } - - - /** - * Parses an {@link InvocationMatcher}. - * - * @param sig A signature in the format described on this class - * - * @return A sig matcher - * - * @throws IllegalArgumentException If the signature is malformed (see EBNF) - * @throws NullPointerException If the signature is null - */ - public static InvocationMatcher parse(String sig) { - int i = parseType(sig, 0); - final TypeMatcher qualifierMatcher = newMatcher(sig.substring(0, i)); - i = consumeChar(sig, i, '#'); - final int nameStart = i; - i = parseSimpleName(sig, i); - final String methodName = sig.substring(nameStart, i); - i = consumeChar(sig, i, '('); - if (isChar(sig, i, ')')) { - return new InvocationMatcher(qualifierMatcher, methodName, Collections.emptyList()); - } - // (_*) matches any argument list - List argMatchers; - if (isChar(sig, i, '_') - && isChar(sig, i + 1, '*') - && isChar(sig, i + 2, ')')) { - argMatchers = null; - i = i + 3; - } else { - argMatchers = new ArrayList<>(); - i = parseArgList(sig, i, argMatchers); - } - if (i != sig.length()) { - throw new IllegalArgumentException("Not a valid signature " + sig); - } - return new InvocationMatcher(qualifierMatcher, methodName, argMatchers); - } - - private static int parseSimpleName(String sig, final int start) { - int i = start; - while (i < sig.length() && Character.isJavaIdentifierPart(sig.charAt(i))) { - i++; - } - if (i == start) { - throw new IllegalArgumentException("Not a valid signature " + sig); - } - return i; - } - - private static int parseArgList(String sig, int i, List argMatchers) { - while (i < sig.length()) { - i = parseType(sig, i, argMatchers); - if (isChar(sig, i, ')')) { - return i + 1; - } - i = consumeChar(sig, i, ','); - } - throw new IllegalArgumentException("Not a valid signature " + sig); - } - - private static int consumeChar(String source, int i, char c) { - if (isChar(source, i, c)) { - return i + 1; - } - throw newParseException(source, i, "character '" + c + "'"); - } - - private static RuntimeException newParseException(String source, int i, String expectedWhat) { - final String indent = " "; - String message = "Expected " + expectedWhat + " at index " + i + ":\n"; - message += indent + "\"" + StringUtil.escapeJava(source) + "\"\n"; - message += indent + StringUtils.repeat(' ', i + 1) + '^' + "\n"; - return new IllegalArgumentException(message); - } - - private static boolean isChar(String source, int i, char c) { - return i < source.length() && source.charAt(i) == c; - } - - private static int parseType(String source, int i, List result) { - final int start = i; - i = parseType(source, i); - result.add(newMatcher(source.substring(start, i))); - return i; - } - - private static int parseType(String source, int i) { - final int start = i; - while (i < source.length() && (Character.isJavaIdentifierPart(source.charAt(i)) - || source.charAt(i) == '.')) { - i++; - } - if (i == start) { - throw newParseException(source, i, "type"); - } - - AssertionUtil.assertValidJavaBinaryName(source.substring(start, i)); - // array dimensions - while (isChar(source, i, '[')) { - i = consumeChar(source, i + 1, ']'); - } - return i; - } - - private static TypeMatcher newMatcher(String name) { - return "_".equals(name) ? TypeMatcher.ANY : new TypeMatcher(name); - } - - private static final class TypeMatcher { - - /** Matches any type. */ - public static final TypeMatcher ANY = new TypeMatcher(null); - - final @Nullable String name; - - private TypeMatcher(@Nullable String name) { - this.name = name; - } - - boolean matches(JTypeMirror type, boolean exact) { - if (name == null) { - return true; - } - return exact ? TypeTestUtil.isExactlyAOrAnon(name, type) == OptionalBool.YES - : TypeTestUtil.isA(name, type); - } - } - } - } diff --git a/pmd-java/src/main/resources/category/java/codestyle.xml b/pmd-java/src/main/resources/category/java/codestyle.xml index a0b0bbd29a..b758d9df32 100644 --- a/pmd-java/src/main/resources/category/java/codestyle.xml +++ b/pmd-java/src/main/resources/category/java/codestyle.xml @@ -1204,18 +1204,28 @@ public class SomeClass { -Checks for variables that are defined before they might be used. A reference is deemed to be premature if it is created right before a block of code that doesn't use it that also has the ability to return or throw an exception. +Checks for variables that are defined before they might be used. A declaration is +deemed to be premature if there are some statements that may return or throw an +exception between the time the variable is declared and the time it is first read. + +Some variables cannot be declared close to their first usage because of side-effects +occurring before they're first used. We try to avoid reporting those by considering +most method and constructor invocations to be impure. See the second example. + +Note that this rule is meant to improve code readability but is not an optimization. +A smart JIT will not care whether the variable is declared prematurely or not, as it +can reorder code. 3 + + + diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/codestyle/PrematureDeclarationTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/codestyle/PrematureDeclarationTest.java index 62dbb60691..c73a9f9781 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/codestyle/PrematureDeclarationTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/rule/codestyle/PrematureDeclarationTest.java @@ -6,7 +6,6 @@ package net.sourceforge.pmd.lang.java.rule.codestyle; import net.sourceforge.pmd.testframework.PmdRuleTst; -@org.junit.Ignore("Rule has not been updated yet") public class PrematureDeclarationTest extends PmdRuleTst { // no additional unit tests } diff --git a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/types/InvocationMatcherTest.java b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/types/InvocationMatcherTest.java index d6f72183d5..626a0f6206 100644 --- a/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/types/InvocationMatcherTest.java +++ b/pmd-java/src/test/java/net/sourceforge/pmd/lang/java/types/InvocationMatcherTest.java @@ -4,7 +4,7 @@ package net.sourceforge.pmd.lang.java.types; -import static net.sourceforge.pmd.lang.java.types.TypeTestUtil.InvocationMatcher.parse; +import static net.sourceforge.pmd.lang.java.types.InvocationMatcher.parse; import static org.hamcrest.Matchers.equalTo; import org.hamcrest.MatcherAssert; diff --git a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/codestyle/xml/PrematureDeclaration.xml b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/codestyle/xml/PrematureDeclaration.xml index 7b6a70b47b..c9eff3cef4 100644 --- a/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/codestyle/xml/PrematureDeclaration.xml +++ b/pmd-java/src/test/resources/net/sourceforge/pmd/lang/java/rule/codestyle/xml/PrematureDeclaration.xml @@ -1,8 +1,8 @@ + 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"> premature declaration before unrelated test @@ -11,18 +11,18 @@ public class Bar { public int lengthSumOf(String[] strings) { - int sum = 0; // wasted cycles if strings have problems + int sum = 0; // premature if (strings == null || strings.length == 0) return 0; - for (int i=0; i + ]]> @@ -30,6 +30,7 @@ public class Bar { 0 #1067 PrematureDeclaration lambda closure false positive 0 #1108 PrematureDeclaration lambda false positive 0 Optional ofRunnable(Supplier sup) {return Optional.of(sup.get());} + private static String sign(String input) {return input.toLowerCase();} + + public String lengthSumOf(String a, String b) { + String signingInput = Stream.of(a, b) + .filter(Objects::nonNull) + .map(String::valueOf) + .collect(Collectors.joining()); + + return ofRunnable(() -> sign(signingInput)) + .orElse(null); + } +} + ]]> + + + PrematureDeclaration with captured lambda var + 0 + Optional ofRunnable(Supplier sup) {return Optional.of(sup.get());} + private static String sign(String input) {return input.toLowerCase();} + + public String lengthSumOf(String a, String b) { + String signingInput = Stream.of(a, b) + .filter(Objects::nonNull) + .map(String::valueOf) + .collect(Collectors.joining()); + + return ofRunnable(() -> { + return sign(signingInput); + }).orElse(null); + } +} + ]]> + + + + PrematureDeclaration with captured lambda var (2) + 0 + Optional ofRunnable(Supplier sup) {return Optional.of(sup);} + private static String sign(String input) {return input.toLowerCase();} + + public boolean lengthSumOf(boolean sign) { String signingInput = Stream.of(a, b) .filter(Objects::nonNull) .map(String::valueOf) .collect(Collectors.joining(EMPTY)); + if (sign) + return ofRunnable(() -> { + return sign(signingInput); // this should be recognized as a usage + }).orElse(false); - return Try.of(() -> sign(signingInput)) - .getOrElse(() -> null); + return signingInput; } } - ]]> + ]]> PrematureDeclaration should also check inside lambdas 1 { +public class PrematureDeclarationLambda { + private static Optional ofRunnable(Supplier sup) {return Optional.of(sup);} + public boolean lengthSumOf(String[] strings) { + + return ofRunnable(() -> { // Inside that lambda *is* a premature declaration of `sum` int sum = 0; @@ -176,9 +247,252 @@ public class PrematureDeclarationLambda { } return sum; - }); + }).isPresent(); } } ]]> + + #1429 PrematureDeclaration should try to recognize side-effects + 0 + cache = new HashMap<>(); + final AtomicInteger ref = new AtomicInteger(); + + public String doSomeThing(String uri, String cachedPath, String ref) { + var prior = cache.put(uri, cachedPath); // may side-effect + + if (ref == null) { + return createRef(cachedPath, 1); // may side-effect + } + if (prior == null) { + return ref.substring(1); + } + return ref; + } + + static String createRef(String cachedPath, int refCount) { + return "" + cachedPath + refCount; + } + } + ]]> + + + #1429 PrematureDeclaration side-effect of time methods + 0 + + + + #1429 PrematureDeclaration side-effect in initializer (control) + 1 + + + + FN with blank local declaration + + 1 + + Declaration of 'scriptClassToExecute' can be moved closer to its usages + + scriptClass; + Object cachedResult; + + private void doScript(Class klass) {} + + public Object getScriptedObject() { + synchronized (this) { + Class scriptClassToExecute; // here + + if (this.cachedResult != null) { + Object result = this.cachedResult; + this.cachedResult = null; + return result; + } + + scriptClassToExecute = this.scriptClass; // here + return doScript(scriptClassToExecute); + } + } + } + ]]> + + + FN with exception ctor flagged as side-effect + + 1 + + Declaration of 'file2' can be moved closer to its usages + + scriptClass; + Object cachedResult; + + private void doScript(Class klass) {} + + + public String combine(String pattern1, String pattern2) { + String ext1 = pattern1.substring(starDotPos1 + 1); + int dotPos2 = pattern2.indexOf('.'); + String file2 = dotPos2 == -1 ? pattern2 : pattern2.substring(0, dotPos2); + String ext2 = dotPos2 == -1 ? "" : pattern2.substring(dotPos2); + boolean ext1All = ext1.equals(".*") || ext1.equals(""); + boolean ext2All = ext2.equals(".*") || ext2.equals(""); + if (!ext1All && !ext2All) { + throw new IllegalArgumentException("Cannot combine patterns: " + pattern1 + " vs " + pattern2); + } + String ext = ext1All ? ext2 : ext1; + return file2 + ext; + } + } + ]]> + + + FN with trivial initializers + + 1 + + Declaration of 'isReal' can be moved closer to its usages + + + + + + FP with references to fields in initializers + 0 + + + + FP with local reassignment + + 0 + parse(String value) throws IllegalArgumentException { + if (!value.isEmpty()) { + return Collections.emptySet(); + } + // obviously this cannot be reordered + // We should also track side effects on locals + String originalValue = value; + value = value.trim(); + if ("ALL".equalsIgnoreCase(value)) { + return EnumSet.allOf(TestGroup.class); + } + if (value.toUpperCase().startsWith("ALL-")) { + Set groups = EnumSet.allOf(TestGroup.class); + groups.remove(TestGroup.A); + return groups; + } + return parseGroups(originalValue, value); + } + } + ]]> +