| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.netbeans.modules.java.hints; |
| |
| import com.sun.source.tree.AssignmentTree; |
| import com.sun.source.tree.BinaryTree; |
| import com.sun.source.tree.BlockTree; |
| import com.sun.source.tree.CompoundAssignmentTree; |
| import com.sun.source.tree.ExpressionTree; |
| import com.sun.source.tree.IdentifierTree; |
| import com.sun.source.tree.IfTree; |
| import com.sun.source.tree.MemberSelectTree; |
| import com.sun.source.tree.ModifiersTree; |
| import com.sun.source.tree.ParenthesizedTree; |
| import com.sun.source.tree.StatementTree; |
| import com.sun.source.tree.SynchronizedTree; |
| import com.sun.source.tree.Tree; |
| import com.sun.source.tree.Tree.Kind; |
| import com.sun.source.tree.VariableTree; |
| import com.sun.source.util.TreePath; |
| import com.sun.source.util.TreePathScanner; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import javax.lang.model.SourceVersion; |
| import javax.lang.model.element.Element; |
| import javax.lang.model.element.ElementKind; |
| import javax.lang.model.element.Modifier; |
| import javax.lang.model.element.VariableElement; |
| import org.netbeans.api.java.source.CompilationInfo; |
| import org.netbeans.api.java.source.TreeMaker; |
| import org.netbeans.api.java.source.TreePathHandle; |
| import org.netbeans.api.java.source.WorkingCopy; |
| import org.netbeans.modules.java.hints.errors.Utilities; |
| import org.netbeans.modules.java.hints.introduce.Flow; |
| import org.netbeans.modules.java.hints.introduce.Flow.FlowResult; |
| import org.netbeans.spi.java.hints.Hint; |
| import org.netbeans.spi.java.hints.TriggerTreeKind; |
| import org.netbeans.spi.java.hints.HintContext; |
| import org.netbeans.spi.java.hints.JavaFix; |
| import org.netbeans.spi.java.hints.ErrorDescriptionFactory; |
| import org.netbeans.spi.editor.hints.ErrorDescription; |
| import org.netbeans.spi.editor.hints.Fix; |
| import org.openide.filesystems.FileObject; |
| import org.openide.util.NbBundle; |
| |
| /** |
| * The hint detects double-locking idion that cannot work with JDK < 5. |
| * It also detects a variant that first assigns the field into a local variable first. |
| * In JDK 5, a recommended idiom can be generated |
| * @author Jaroslav tulach |
| */ |
| @Hint(displayName = "#DN_org.netbeans.modules.java.hints.DoubleCheck", description = "#DESC_org.netbeans.modules.java.hints.DoubleCheck", id="org.netbeans.modules.java.hints.DoubleCheck", category="thread", suppressWarnings="DoubleCheckedLocking") |
| public class DoubleCheck { |
| |
| @TriggerTreeKind(Kind.SYNCHRONIZED) |
| public static ErrorDescription run(HintContext ctx) { |
| CompilationInfo compilationInfo = ctx.getInfo(); |
| TreePath treePath = ctx.getPath(); |
| Tree e = treePath.getLeaf(); |
| |
| SynchronizedTree synch = (SynchronizedTree)e; |
| TreePath outer = findOuterIf(ctx, treePath); |
| if (outer == null) { |
| return null; |
| } |
| |
| IfTree same = null; |
| TreePath samePath = null; |
| TreePath block = new TreePath(treePath, synch.getBlock()); |
| TreePath exprPath = null; |
| TreePath[] fieldPath = new TreePath[1]; |
| for (StatementTree statement : synch.getBlock().getStatements()) { |
| samePath = new TreePath(block, statement); |
| exprPath = sameIfAndValidate(compilationInfo, samePath, outer, fieldPath); |
| if (exprPath != null) { |
| same = (IfTree)statement; |
| break; |
| } |
| if (ctx.isCanceled()) { |
| return null; |
| } |
| } |
| if (same == null) { |
| return null; |
| } |
| Element el = compilationInfo.getTrees().getElement(fieldPath[0]); |
| if (el == null) { |
| return null; |
| } |
| Element checkEl = compilationInfo.getTrees().getElement(exprPath); |
| |
| boolean jdk5 = compilationInfo.getSourceVersion().compareTo(SourceVersion.RELEASE_5) >= 0; |
| boolean checkLocalVar = checkEl != null && checkEl.getKind() == ElementKind.LOCAL_VARIABLE; |
| boolean varVolatile = el.getModifiers().contains(Modifier.VOLATILE); |
| boolean field = el.getKind() == ElementKind.FIELD; |
| |
| Fix fix = null; |
| |
| if (!varVolatile || !jdk5 || !field) { |
| // the field is not volatile or it cannot be resolved from the code [local var cannot be volatile] |
| // the only fix available when JDK5 is missing |
| fix = new SynchronizeFix( |
| TreePathHandle.create(treePath, compilationInfo), |
| TreePathHandle.create(outer, compilationInfo), |
| compilationInfo.getFileObject() |
| ).toEditorFix(); |
| } |
| |
| Fix fix2 = null; |
| // can be only applied the real field is known |
| if (jdk5 && field && !(checkLocalVar && varVolatile)) { |
| int style; |
| if (checkLocalVar && !varVolatile) { |
| style = 0; |
| } else if (varVolatile && !checkLocalVar) { |
| style = 2; |
| } else { |
| // default JDK5 idiom |
| style = 1; |
| } |
| fix2 = new DoubleCheckJDK5Fix( |
| TreePathHandle.create(outer, compilationInfo), |
| TreePathHandle.create(exprPath, compilationInfo), |
| TreePathHandle.create(fieldPath[0], compilationInfo), |
| TreePathHandle.create(samePath, compilationInfo), |
| style).toEditorFix(); |
| } |
| int span = (int)compilationInfo.getTrees().getSourcePositions().getStartPosition( |
| compilationInfo.getCompilationUnit(), |
| synch |
| ); |
| if (fix == null && fix2 == null) { |
| return null; |
| } |
| |
| return ErrorDescriptionFactory.forName(ctx, ctx.getPath(), NbBundle.getMessage(DoubleCheck.class, "ERR_DoubleCheck"), fix2, fix);// NOI18N |
| } |
| |
| private static TreePath findOuterIf(HintContext ctx, TreePath treePath) { |
| while (!ctx.isCanceled()) { |
| treePath = treePath.getParentPath(); |
| if (treePath == null) { |
| break; |
| } |
| Tree leaf = treePath.getLeaf(); |
| |
| if (leaf.getKind() == Kind.IF) { |
| return treePath; |
| } |
| |
| if (leaf.getKind() == Kind.BLOCK) { |
| BlockTree b = (BlockTree)leaf; |
| if (b.getStatements().size() == 1) { |
| // ok, empty blocks can be around synchronized(this) |
| // statements |
| continue; |
| } |
| } |
| |
| return null; |
| } |
| return null; |
| } |
| |
| @SuppressWarnings("AssignmentToMethodParameter") |
| private static TreePath findParentOfKind(TreePath p, Tree.Kind kind) { |
| while (p != null) { |
| if (p.getLeaf().getKind() == kind) { |
| return p; |
| } |
| p = p.getParentPath(); |
| } |
| return p; |
| } |
| |
| private static boolean sameCompilationUnit(TreePath first, TreePath second) { |
| TreePath one = findParentOfKind(first, Kind.COMPILATION_UNIT); |
| TreePath two = findParentOfKind(second, Kind.COMPILATION_UNIT); |
| return one != null && two != null && one.getLeaf() == two.getLeaf(); |
| } |
| |
| /** |
| * Checks, that the two if statements test the same variable. Returns the tree that references |
| * the guard variable if the two ifs are the same, or {@code null} if the ifs do not match. |
| * @param info |
| * @param statementTP |
| * @param secondTP |
| * @return |
| */ |
| private static TreePath sameIfAndValidate(CompilationInfo info, TreePath statementTP, TreePath secondTP, TreePath[] fieldRef) { |
| StatementTree statement = (StatementTree) statementTP.getLeaf(); |
| |
| if (statement.getKind() != Kind.IF) { |
| return null; |
| } |
| |
| IfTree first = (IfTree)statement; |
| IfTree second = (IfTree) secondTP.getLeaf(); |
| |
| if (first.getElseStatement() != null) { |
| return null; |
| } |
| if (second.getElseStatement() != null) { |
| return null; |
| } |
| |
| TreePath varFirst = equalToNull(new TreePath(statementTP, first.getCondition())); |
| TreePath varSecond = equalToNull(new TreePath(secondTP, second.getCondition())); |
| |
| if (varFirst == null || varSecond == null) { |
| return null; |
| } |
| |
| Element firstVariable = info.getTrees().getElement(varFirst); |
| Element secondVariable = info.getTrees().getElement(varSecond); |
| Element target = firstVariable; |
| if (firstVariable != null && firstVariable.equals(secondVariable)) { |
| TreePath var = info.getTrees().getPath(firstVariable); |
| if (info.getSourceVersion().compareTo(SourceVersion.RELEASE_5) < 0) { |
| fieldRef[0] = var; |
| return varFirst; |
| } |
| if (firstVariable.getKind() == ElementKind.LOCAL_VARIABLE) { |
| // check how the variable was assigned: |
| TreePath methodPath = Utilities.findTopLevelBlock(varFirst); |
| FlowResult fr = Flow.assignmentsForUse(info, methodPath, new AtomicBoolean(false)); |
| Iterable<? extends TreePath> itp = fr.getAssignmentsForUse().get(varFirst.getLeaf()); |
| |
| if (itp != null) { |
| Iterator<? extends TreePath> i = itp.iterator(); |
| if (i.hasNext()) { |
| TreePath v = i.next(); |
| if (!i.hasNext()) { |
| // if the local variable has exactly one possible value, |
| // use it as the field |
| target = info.getTrees().getElement(v); |
| if (target != null && target.getKind() == ElementKind.FIELD) { |
| var = info.getTrees().getPath(target); |
| if (!sameCompilationUnit(var, varFirst)) { |
| // the variable is somewhere ... |
| var = info.getTrees().getPath(firstVariable); |
| } |
| } |
| } |
| } |
| } |
| } |
| fieldRef[0] = var; |
| return varFirst; |
| } |
| |
| return null; |
| } |
| |
| private static TreePath equalToNull(TreePath tp) { |
| ExpressionTree t = (ExpressionTree) tp.getLeaf(); |
| if (t.getKind() == Kind.PARENTHESIZED) { |
| ParenthesizedTree p = (ParenthesizedTree)t; |
| t = p.getExpression(); |
| tp = new TreePath(tp, t); |
| } |
| |
| if (t.getKind() != Kind.EQUAL_TO) { |
| return null; |
| } |
| BinaryTree bt = (BinaryTree)t; |
| if (bt.getLeftOperand().getKind() == Kind.NULL_LITERAL && bt.getRightOperand().getKind() != Kind.NULL_LITERAL) { |
| return new TreePath(tp, bt.getRightOperand()); |
| } |
| if (bt.getLeftOperand().getKind() != Kind.NULL_LITERAL && bt.getRightOperand().getKind() == Kind.NULL_LITERAL) { |
| return new TreePath(tp, bt.getLeftOperand()); |
| } |
| return null; |
| } |
| |
| /** |
| * This fix will declare the checked field volatile, if it is not already. |
| * Assigns the field into a local variable |
| */ |
| private static final class DoubleCheckJDK5Fix extends JavaFix { |
| private final TreePathHandle fieldAccessHandle; |
| private final TreePathHandle fieldHandle; |
| private final TreePathHandle innerIfHandle; |
| private final int justVolatile; |
| |
| private FlowResult flow; |
| |
| public DoubleCheckJDK5Fix(TreePathHandle handle, TreePathHandle fieldAccessHandle, TreePathHandle fieldHandle, TreePathHandle innerIfHandle, |
| int justVolatile) { |
| super(handle); |
| this.fieldAccessHandle = fieldAccessHandle; |
| this.fieldHandle = fieldHandle; |
| this.innerIfHandle = innerIfHandle; |
| this.justVolatile = justVolatile; |
| } |
| |
| @Override |
| protected String getText() { |
| switch (justVolatile) { |
| case 0: |
| return NbBundle.getMessage(DoubleCheck.class, "FIX_DoubleCheck_Volatile"); |
| case 1: |
| return NbBundle.getMessage(DoubleCheck.class, "FIX_DoubleCheck_5"); // NOI18N |
| case 2: |
| return NbBundle.getMessage(DoubleCheck.class, "FIX_DoubleCheck_Local"); // NOI18N |
| default: |
| throw new IllegalStateException(); |
| } |
| } |
| |
| @Override |
| protected void performRewrite(TransformationContext ctx) throws Exception { |
| WorkingCopy wc = ctx.getWorkingCopy(); |
| TreeMaker mk = wc.getTreeMaker(); |
| TreePath varPath = fieldAccessHandle.resolve(wc); |
| TreePath fieldPath = fieldHandle.resolve(wc); |
| |
| Element ve = wc.getTrees().getElement(fieldPath); |
| if (ve == null) { |
| // TODO: log/inform the user |
| return; |
| } |
| VariableTree vt = (VariableTree)fieldPath.getLeaf(); |
| |
| if (!vt.getModifiers().getFlags().contains(Modifier.VOLATILE)) { |
| ModifiersTree mt = wc.getTreeMaker().addModifiersModifier(vt.getModifiers(), Modifier.VOLATILE); |
| wc.rewrite(vt.getModifiers(), mt); |
| } |
| Element classEl = ((VariableElement)ve).getEnclosingElement(); |
| if (justVolatile == 0) { |
| // the checked symbol is already a variable |
| return; |
| } |
| // construct the reference to the field. If the field is referenced just using the |
| // simple name, combine it with [outerClass.]this. If the reference is more complex, |
| // it is qualified enough and can be used as it is |
| ExpressionTree fieldAccess = (ExpressionTree)varPath.getLeaf(); |
| if (fieldAccess.getKind() == Tree.Kind.IDENTIFIER) { |
| if (ve.getModifiers().contains(Modifier.STATIC)) { |
| fieldAccess = mk.MemberSelect(mk.QualIdent(classEl), vt.getName()); |
| } else { |
| fieldAccess = mk.MemberSelect(mk.Identifier("this"), vt.getName()); |
| } |
| } |
| |
| TreePath ifStatement = ctx.getPath(); |
| TreePath ifParent = ifStatement.getParentPath(); |
| BlockTree blTree = (BlockTree)ifParent.getLeaf(); |
| VariableTree localCopy = mk.Variable( |
| mk.Modifiers(Collections.<Modifier>emptySet()), vt.getName(), |
| vt.getType(), fieldAccess); |
| |
| // insert the variable declaration before the if statement: |
| int ifIndex = blTree.getStatements().indexOf(ifStatement.getLeaf()); |
| BlockTree newBlock = mk.insertBlockStatement(blTree, ifIndex, localCopy); |
| wc.rewrite(blTree, newBlock); |
| |
| TreePath innerIf = innerIfHandle.resolve(wc); |
| TreePath innerBlock = innerIf.getParentPath(); |
| assert innerBlock.getLeaf().getKind() == Tree.Kind.BLOCK; |
| BlockTree bt = (BlockTree)innerBlock.getLeaf(); |
| int innerIfIndex = bt.getStatements().indexOf(innerIf.getLeaf()); |
| |
| // add a second assignment to the local variable |
| BlockTree nbt = mk.insertBlockStatement(bt, innerIfIndex, |
| mk.ExpressionStatement( |
| mk.Assignment(mk.Identifier(vt.getName()), fieldAccess) |
| )); |
| wc.rewrite(bt, nbt); |
| TreePath topLevel = Utilities.findTopLevelBlock(ifParent); |
| flow = Flow.assignmentsForUse(wc, topLevel, new AtomicBoolean(false)); |
| |
| VariableAccessWalker walker = new VariableAccessWalker((VariableElement)ve, |
| fieldAccess, mk.Identifier(localCopy.getName()), ifStatement, wc); |
| walker.scan(topLevel, null); |
| } |
| } |
| |
| private static final class VariableAccessWalker extends TreePathScanner { |
| private final VariableElement originalVariable; |
| private final ExpressionTree fieldAccessTree; |
| /** |
| * Tree that references the local variable, will be used instead of |
| * field access for reading |
| */ |
| private final ExpressionTree localVarTree; |
| private final TreePath ifPath; |
| private final WorkingCopy wc; |
| private final TreeMaker mk; |
| |
| public VariableAccessWalker(VariableElement originalVariable, ExpressionTree fieldAccessTree, ExpressionTree localVarTree, TreePath ifPath, WorkingCopy wc) { |
| this.originalVariable = originalVariable; |
| this.fieldAccessTree = fieldAccessTree; |
| this.localVarTree = localVarTree; |
| this.ifPath = ifPath; |
| this.wc = wc; |
| this.mk = wc.getTreeMaker(); |
| } |
| |
| // will turn to true once the outer `if' is traversed. The following code should |
| // |
| private boolean ifEncountered; |
| |
| /** |
| @Override |
| public Object scan(TreePath p, Object o) { |
| // after the outer if is encountered, direct every reference to local variable |
| ifEncountered |= p.getLeaf() == ifPath.getLeaf(); |
| return super.scan(p, o); |
| } |
| */ |
| |
| @Override |
| public Object scan(Tree p, Object o) { |
| // after the outer if is encountered, direct every reference to local variable |
| ifEncountered |= p == ifPath.getLeaf(); |
| return super.scan(p, o); |
| } |
| |
| |
| @Override |
| public Object visitIdentifier(IdentifierTree node, Object p) { |
| if (ifEncountered) { |
| // inside the inner if-statement, all but assignments should transform to local variable references. |
| Element el = wc.getTrees().getElement(getCurrentPath()); |
| if (el == originalVariable) { |
| // rewrite the member select to whatever tree access is necessary |
| wc.rewrite(node, localVarTree); |
| // skip the member select |
| return null; |
| } |
| } |
| return super.visitIdentifier(node, p); //To change body of generated methods, choose Tools | Templates. |
| } |
| |
| @Override |
| public Object visitMemberSelect(MemberSelectTree node, Object p) { |
| if (ifEncountered) { |
| // inside the inner if-statement, all but assignments should transform to local variable references. |
| Element el = wc.getTrees().getElement(getCurrentPath()); |
| if (el == originalVariable) { |
| // rewrite the member select to whatever tree access is necessary |
| wc.rewrite(node, localVarTree); |
| // skip the member select |
| return null; |
| } |
| } |
| return super.visitMemberSelect(node, p); |
| } |
| |
| @Override |
| public Object visitCompoundAssignment(CompoundAssignmentTree node, Object p) { |
| // TODO: it's not necessary for reference-type variables; might be eventually used |
| // for primitives |
| return super.visitCompoundAssignment(node, p); |
| } |
| |
| @Override |
| public Object visitAssignment(AssignmentTree node, Object p) { |
| if (!ifEncountered) { |
| return super.visitAssignment(node, p); |
| } |
| Element el = wc.getTrees().getElement(new TreePath(getCurrentPath(), node.getVariable())); |
| if (el == originalVariable) { |
| // assignment should be converted into local var + field assignment |
| ExpressionTree et = mk.Assignment(fieldAccessTree, |
| mk.Assignment(localVarTree, node.getExpression())); |
| wc.rewrite(node, et); |
| } |
| return scan(node.getExpression(), p); |
| } |
| |
| |
| } |
| |
| private static final class SynchronizeFix extends JavaFix { |
| private TreePathHandle synchHandle; |
| private FileObject file; |
| |
| public SynchronizeFix(TreePathHandle synchHandle, TreePathHandle ifHandle, FileObject file) { |
| super(ifHandle); |
| this.synchHandle = synchHandle; |
| this.file = file; |
| } |
| |
| |
| public String getText() { |
| return NbBundle.getMessage(DoubleCheck.class, "FIX_DoubleCheck"); // NOI18N |
| } |
| |
| @Override public String toString() { |
| return "FixDoubleCheck"; // NOI18N |
| } |
| |
| @Override |
| protected void performRewrite(TransformationContext ctx) { |
| WorkingCopy wc = ctx.getWorkingCopy(); |
| TreePath ifTreePath = ctx.getPath(); |
| Tree syncTree = synchHandle.resolve(wc).getLeaf(); |
| wc.rewrite(ifTreePath.getLeaf(), syncTree); |
| } |
| } |
| |
| } |