diff --git a/java/java-analysis-impl/src/com/intellij/codeInsight/InferredAnnotationsManagerImpl.java b/java/java-analysis-impl/src/com/intellij/codeInsight/InferredAnnotationsManagerImpl.java index 9191a76cce86..a82b56939a7e 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInsight/InferredAnnotationsManagerImpl.java +++ b/java/java-analysis-impl/src/com/intellij/codeInsight/InferredAnnotationsManagerImpl.java @@ -21,7 +21,8 @@ import static com.intellij.codeInspection.dataFlow.ControlFlowAnalyzer.ORG_JETBR public class InferredAnnotationsManagerImpl extends InferredAnnotationsManager { private static final Set INFERRED_ANNOTATIONS = - ContainerUtil.set(NOT_NULL, NULLABLE, ORG_JETBRAINS_ANNOTATIONS_CONTRACT); + ContainerUtil.set(NOT_NULL, NULLABLE, ORG_JETBRAINS_ANNOTATIONS_CONTRACT, Mutability.UNMODIFIABLE_ANNOTATION, + Mutability.UNMODIFIABLE_VIEW_ANNOTATION); private final Project myProject; public InferredAnnotationsManagerImpl(Project project) { @@ -64,6 +65,10 @@ public class InferredAnnotationsManagerImpl extends InferredAnnotationsManager { return anno == null ? null : annotationFQN.equals(anno.getQualifiedName()) ? anno : null; } + if (Mutability.UNMODIFIABLE_ANNOTATION.equals(annotationFQN) || Mutability.UNMODIFIABLE_VIEW_ANNOTATION.equals(annotationFQN)) { + return getInferredMutabilityAnnotation(listOwner); + } + if (listOwner instanceof PsiMethodImpl && ORG_JETBRAINS_ANNOTATIONS_CONTRACT.equals(annotationFQN)) { return getInferredContractAnnotation((PsiMethodImpl)listOwner); } @@ -97,6 +102,17 @@ public class InferredAnnotationsManagerImpl extends InferredAnnotationsManager { return false; } + @Nullable + private PsiAnnotation getInferredMutabilityAnnotation(@NotNull PsiModifierListOwner owner) { + if (!(owner instanceof PsiMethod)) return null; + PsiModifierList modifiers = ((PsiMethod)owner).getModifierList(); + if (modifiers.findAnnotation(Mutability.UNMODIFIABLE_ANNOTATION) != null || + modifiers.findAnnotation(Mutability.UNMODIFIABLE_VIEW_ANNOTATION) != null) { + return null; + } + return Mutability.inferMutability(owner).asAnnotation(myProject); + } + @Nullable private PsiAnnotation getInferredContractAnnotation(PsiMethodImpl method) { if (method.getModifierList().findAnnotation(ORG_JETBRAINS_ANNOTATIONS_CONTRACT) != null) { @@ -214,6 +230,8 @@ public class InferredAnnotationsManagerImpl extends InferredAnnotationsManager { ContainerUtil.addIfNotNull(result, getInferredNullityAnnotation((PsiParameter)listOwner)); } + ContainerUtil.addIfNotNull(result, getInferredMutabilityAnnotation(listOwner)); + return result.toArray(PsiAnnotation.EMPTY_ARRAY); } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInferenceIndex.kt b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInferenceIndex.kt index 2a498d674e7d..14c7de3ded73 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInferenceIndex.kt +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInferenceIndex.kt @@ -30,7 +30,7 @@ import java.util.* * @author peter */ -private val gist = GistManager.getInstance().newPsiFileGist("contractInference", 6, MethodDataExternalizer) { file -> +private val gist = GistManager.getInstance().newPsiFileGist("contractInference", 7, MethodDataExternalizer) { file -> indexFile(file.node.lighterAST) } @@ -61,7 +61,7 @@ private fun calcData(tree: LighterAST, method: LighterASTNode): MethodData? { val contracts = ContractInferenceInterpreter(tree, method, body).inferContracts(statements) - val nullityVisitor = NullityInference.NullityInferenceVisitor(tree, body) + val nullityVisitor = NullityInference.MethodReturnInferenceVisitor(tree, body) val purityVisitor = PurityInference.PurityInferenceVisitor(tree, body) for (statement in statements) { walkMethodBody(tree, statement) { nullityVisitor.visitNode(it); purityVisitor.visitNode(it) } @@ -85,12 +85,12 @@ private fun walkMethodBody(tree: LighterAST, root: LighterASTNode, processor: (L private fun createData(body: LighterASTNode, contracts: List, - nullity: NullityInferenceResult?, + methodReturn: MethodReturnInferenceResult?, purity: PurityInferenceResult?, notNullParams: BitSet): MethodData? { - if (nullity == null && purity == null && contracts.isEmpty() && notNullParams.isEmpty) return null + if (methodReturn == null && purity == null && contracts.isEmpty() && notNullParams.isEmpty) return null - return MethodData(nullity, purity, contracts, notNullParams, body.startOffset, body.endOffset) + return MethodData(methodReturn, purity, contracts, notNullParams, body.startOffset, body.endOffset) } fun getIndexedData(method: PsiMethodImpl): MethodData? = gist.getFileData(method.containingFile)?.get(JavaStubImplUtil.getMethodStubIndex(method)) \ No newline at end of file diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaFactType.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaFactType.java index ef0b447d21b7..8244d4a03c59 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaFactType.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/DfaFactType.java @@ -84,12 +84,18 @@ public abstract class DfaFactType extends Key { } }; - public static final DfaFactType MUTABILITY = new DfaFactType("Mutable") { + public static final DfaFactType MUTABILITY = new DfaFactType("Mutability") { @Override boolean isUnknown(@NotNull Mutability fact) { return fact == Mutability.UNKNOWN; } + @NotNull + @Override + Mutability unionFacts(@NotNull Mutability left, @NotNull Mutability right) { + return left.union(right); + } + @NotNull @Override Mutability calcFromVariable(@NotNull DfaVariableValue value) { diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/MethodDataExternalizer.kt b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/MethodDataExternalizer.kt index bb149170c38c..a67a7b6ec836 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/MethodDataExternalizer.kt +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/MethodDataExternalizer.kt @@ -33,7 +33,7 @@ internal object MethodDataExternalizer : DataExternalizer> override fun read(input: DataInput) = readSeq(input) { readINT(input) to readMethod(input) }.toMap() private fun writeMethod(out: DataOutput, data: MethodData) { - writeNullable(out, data.nullity) { writeNullity(out, it) } + writeNullable(out, data.methodReturn) { writeNullity(out, it) } writeNullable(out, data.purity) { writePurity(out, it) } writeSeq(out, data.contracts) { writeContract(out, it) } writeBitSet(out, data.notNullParameters) @@ -65,14 +65,16 @@ internal object MethodDataExternalizer : DataExternalizer> return BitSet.valueOf(bytes) } - private fun writeNullity(out: DataOutput, nullity: NullityInferenceResult) = when (nullity) { - is NullityInferenceResult.Predefined -> { out.writeByte(0); out.writeByte(nullity.value.ordinal) } - is NullityInferenceResult.FromDelegate -> { out.writeByte(1); writeRanges(out, nullity.delegateCalls) } - else -> throw IllegalArgumentException(nullity.toString()) + private fun writeNullity(out: DataOutput, methodReturn: MethodReturnInferenceResult) = when (methodReturn) { + is MethodReturnInferenceResult.Predefined -> { out.writeByte(0); out.writeByte(methodReturn.value.ordinal) } + is MethodReturnInferenceResult.FromDelegate -> { + out.writeByte(1); out.writeByte(methodReturn.value.ordinal); writeRanges(out, methodReturn.delegateCalls) + } + else -> throw IllegalArgumentException(methodReturn.toString()) } - private fun readNullity(input: DataInput): NullityInferenceResult = when (input.readByte().toInt()) { - 0 -> NullityInferenceResult.Predefined(Nullness.values()[input.readByte().toInt()]) - else -> NullityInferenceResult.FromDelegate(readRanges(input)) + private fun readNullity(input: DataInput): MethodReturnInferenceResult = when (input.readByte().toInt()) { + 0 -> MethodReturnInferenceResult.Predefined(Nullness.values()[input.readByte().toInt()]) + else -> MethodReturnInferenceResult.FromDelegate(Nullness.values()[input.readByte().toInt()], readRanges(input)) } private fun writeRanges(out: DataOutput, ranges: List) = writeSeq(out, ranges) { writeRange(out, it) } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/Mutability.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/Mutability.java index ce85819983b0..8ba557fd47f5 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/Mutability.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/Mutability.java @@ -4,42 +4,85 @@ package com.intellij.codeInspection.dataFlow; import com.intellij.codeInsight.AnnotationUtil; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.ModificationTracker; +import com.intellij.openapi.util.RecursionManager; import com.intellij.psi.*; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.psi.util.PsiUtil; +import com.intellij.psi.impl.source.PsiMethodImpl; +import com.intellij.psi.util.*; +import com.intellij.testFramework.LightVirtualFile; import com.intellij.util.ObjectUtils; +import com.siyeh.ig.psiutils.ClassUtils; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Collections; +import static com.intellij.codeInspection.bytecodeAnalysis.ProjectBytecodeAnalysis.INFERRED_ANNOTATION; + public enum Mutability { /** * Mutability is not known; probably value can be mutated */ - UNKNOWN, + UNKNOWN("Unknown", null), /** * A value is known to be mutable (e.g. elements are sometimes added to the collection) */ - MUTABLE, + MUTABLE("Modifiable", null), /** * A value is known to be immutable. For collection no elements could be added, removed or altered (though if collection * contains mutable elements, they still could be mutated). */ - UNMODIFIABLE, + UNMODIFIABLE("Unmodifiable", "org.jetbrains.annotations.Unmodifiable"), /** * A value is known to be an immutable view over a possibly mutable value: it cannot be mutated directly using this * reference; however subsequent reads (e.g. {@link java.util.Collection#size}) may return different results if the * underlying value is mutated by somebody else. */ - UNMODIFIABLE_VIEW; + UNMODIFIABLE_VIEW("Unmodifiable view", "org.jetbrains.annotations.UnmodifiableView"); - public static final String UNMODIFIABLE_ANNOTATION = "org.jetbrains.annotations.Unmodifiable"; - public static final String UNMODIFIABLE_VIEW_ANNOTATION = "org.jetbrains.annotations.UnmodifiableView"; + public static final @NotNull String UNMODIFIABLE_ANNOTATION = UNMODIFIABLE.myAnnotation; + public static final @NotNull String UNMODIFIABLE_VIEW_ANNOTATION = UNMODIFIABLE_VIEW.myAnnotation; + private final String myName; + private final String myAnnotation; + private final Key> myKey; + + Mutability(String name, String annotation) { + myName = name; + myAnnotation = annotation; + myKey = annotation == null ? null : Key.create(annotation); + } + + @Override + public String toString() { + return myName; + } public boolean isUnmodifiable() { return this == UNMODIFIABLE || this == UNMODIFIABLE_VIEW; } + @NotNull + public Mutability union(Mutability other) { + if (this == other) return this; + if (this == MUTABLE || other == MUTABLE) return MUTABLE; + if (this == UNKNOWN || other == UNKNOWN) return UNKNOWN; + if (this == UNMODIFIABLE_VIEW || other == UNMODIFIABLE_VIEW) return UNMODIFIABLE_VIEW; + return UNMODIFIABLE; + } + + @Nullable + public PsiAnnotation asAnnotation(Project project) { + if (myAnnotation == null) return null; + return CachedValuesManager.getManager(project).getCachedValue(project, myKey, () -> { + PsiAnnotation annotation = JavaPsiFacade.getElementFactory(project).createAnnotationFromText("@" + myAnnotation, null); + annotation.putUserData(INFERRED_ANNOTATION, Boolean.TRUE); + ((LightVirtualFile)annotation.getContainingFile().getViewProvider().getVirtualFile()).setWritable(false); + return CachedValueProvider.Result.create(annotation, ModificationTracker.NEVER_CHANGED); + }, false); + } + /** * Returns a mutability of the supplied element, if known. The element could be a method * (in this case the return value mutability is returned), a method parameter @@ -84,9 +127,35 @@ public enum Mutability { PsiExpression initializer = PsiUtil.skipParenthesizedExprDown(((PsiField)owner).getInitializer()); if (initializer instanceof PsiMethodCallExpression) { PsiMethod method = ((PsiMethodCallExpression)initializer).resolveMethod(); - return method == null ? UNKNOWN : getMutability(method); + if (method == null) { + return UNKNOWN; + } + if (ClassUtils.isImmutable(method.getReturnType())) return UNMODIFIABLE; + return getMutability(method); } } - return UNKNOWN; + return inferMutability(owner); + } + + public static Mutability inferMutability(PsiModifierListOwner owner) { + if (!(owner instanceof PsiMethodImpl)) return UNKNOWN; + PsiMethodImpl method = (PsiMethodImpl)owner; + if (!InferenceFromSourceUtil.shouldInferFromSource(method)) { + return UNKNOWN; + } + + PsiType type = method.getReturnType(); + if (type == null || ClassUtils.isImmutable(type)) { + return UNKNOWN; + } + + return CachedValuesManager.getCachedValue(method, () -> { + MethodData data = ContractInferenceIndexKt.getIndexedData(method); + MethodReturnInferenceResult result = data == null ? null : data.getMethodReturn(); + Mutability mutability = result == null ? null : RecursionManager + .doPreventingRecursion(method, true, () -> result.getMutability(method, data.methodBody(method))); + if (mutability == null) mutability = UNKNOWN; + return CachedValueProvider.Result.create(mutability, method, PsiModificationTracker.JAVA_STRUCTURE_MODIFICATION_COUNT); + }); } } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullityInference.java b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullityInference.java index 313cd2a4aff0..cfda22bd090c 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullityInference.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullityInference.java @@ -60,7 +60,7 @@ public class NullityInference { return CachedValuesManager.getCachedValue(method, () -> { MethodData data = ContractInferenceIndexKt.getIndexedData(method); - NullityInferenceResult result = data == null ? null : data.getNullity(); + MethodReturnInferenceResult result = data == null ? null : data.getMethodReturn(); Nullness nullness = result == null ? null : RecursionManager.doPreventingRecursion(method, true, () -> result.getNullness(method, data.methodBody(method))); if (nullness == null) nullness = Nullness.UNKNOWN; return CachedValueProvider.Result.create(nullness, method, PsiModificationTracker.JAVA_STRUCTURE_MODIFICATION_COUNT); @@ -90,7 +90,7 @@ public class NullityInference { }); } - static class NullityInferenceVisitor { + static class MethodReturnInferenceVisitor { private final LighterAST tree; private final LighterASTNode body; private boolean hasErrors; @@ -101,7 +101,7 @@ public class NullityInference { Set assignments = ContainerUtil.newHashSet(); Set returnedCheckedVars = ContainerUtil.newHashSet(); - NullityInferenceVisitor(LighterAST tree, LighterASTNode body) { + MethodReturnInferenceVisitor(LighterAST tree, LighterASTNode body) { this.tree = tree; this.body = body; } @@ -193,15 +193,15 @@ public class NullityInference { private boolean isNonNullCondition(@Nullable LighterASTNode expr, LighterASTNode var) { expr = skipParenthesesCastsDown(tree, expr); if (expr == null) return false; - + IElementType type = expr.getTokenType(); if (type == BINARY_EXPRESSION || type == POLYADIC_EXPRESSION) { List operands = getExpressionChildren(tree, expr); if (firstChildOfType(tree, expr, JavaTokenType.NE) != null) { return operands.size() == 2 && isNullLiteral(operands.get(1)) && isReferenceTo(operands.get(0), var); } - - return firstChildOfType(tree, expr, JavaTokenType.ANDAND) != null && ContainerUtil.exists(operands, e -> isNonNullCondition(e, var)); + + return firstChildOfType(tree, expr, JavaTokenType.ANDAND) != null && ContainerUtil.exists(operands, e -> isNonNullCondition(e, var)); } return type == INSTANCE_OF_EXPRESSION && isReferenceTo(expr, var); @@ -209,8 +209,8 @@ public class NullityInference { private boolean isReferenceTo(@NotNull LighterASTNode expr, @NotNull LighterASTNode var) { LighterASTNode operand = skipParenthesesCastsDown(tree, findExpressionChild(tree, expr)); - if (operand == null || - operand.getTokenType() != REFERENCE_EXPRESSION || + if (operand == null || + operand.getTokenType() != REFERENCE_EXPRESSION || !Objects.equals(getNameIdentifierText(tree, operand), getNameIdentifierText(tree, operand))) { return false; } @@ -222,7 +222,7 @@ public class NullityInference { } @Nullable - NullityInferenceResult getResult() { + MethodReturnInferenceResult getResult() { if (!returnedCheckedVars.isEmpty()) { if (ContainerUtil.exists(returnedCheckedVars, name -> !assignments.contains(name))) { hasNotNulls = true; @@ -230,19 +230,25 @@ public class NullityInference { hasUnknowns = true; } } - + + List delegateCalls = null; + if (delegates.size() == 1) { + delegateCalls = ContainerUtil.newArrayList(delegates.get(delegates.keySet().iterator().next())); + } if (hasNulls) { - return new NullityInferenceResult.Predefined(Nullness.NULLABLE); + return delegateCalls == null || hasNotNulls || hasErrors || hasUnknowns + ? new MethodReturnInferenceResult.Predefined(Nullness.NULLABLE) + : new MethodReturnInferenceResult.FromDelegate(Nullness.NULLABLE, delegateCalls); } if (hasErrors || hasUnknowns || delegates.size() > 1) { return null; } - if (delegates.size() == 1) { - return new NullityInferenceResult.FromDelegate(ContainerUtil.newArrayList(delegates.get(delegates.keySet().iterator().next()))); + if (delegateCalls != null) { + return new MethodReturnInferenceResult.FromDelegate(hasNotNulls ? Nullness.NOT_NULL : Nullness.UNKNOWN, delegateCalls); } if (hasNotNulls) { - return new NullityInferenceResult.Predefined(Nullness.NOT_NULL); + return new MethodReturnInferenceResult.Predefined(Nullness.NOT_NULL); } return null; } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inferenceResults.kt b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inferenceResults.kt index 304a4396a505..5766f2fd408c 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inferenceResults.kt +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inferenceResults.kt @@ -25,6 +25,7 @@ import com.intellij.psi.util.CachedValueProvider import com.intellij.psi.util.CachedValuesManager import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiUtil +import com.siyeh.ig.psiutils.ClassUtils import java.util.* /** @@ -89,11 +90,12 @@ data class PurityInferenceResult(internal val mutatedRefs: List } -interface NullityInferenceResult { +interface MethodReturnInferenceResult { fun getNullness(method: PsiMethod, body: () -> PsiCodeBlock): Nullness + fun getMutability(method: PsiMethod, body: () -> PsiCodeBlock): Mutability = Mutability.UNKNOWN @Suppress("EqualsOrHashCode") - data class Predefined(internal val value: Nullness) : NullityInferenceResult { + data class Predefined(internal val value: Nullness) : MethodReturnInferenceResult { override fun hashCode() = value.ordinal override fun getNullness(method: PsiMethod, body: () -> PsiCodeBlock) = when { value == Nullness.NULLABLE && InferenceFromSourceUtil.suppressNullable(method) -> Nullness.UNKNOWN @@ -101,10 +103,33 @@ interface NullityInferenceResult { } } - data class FromDelegate(internal val delegateCalls: List) : NullityInferenceResult { - override fun getNullness(method: PsiMethod, body: () -> PsiCodeBlock) = when { - delegateCalls.all { range -> isNotNullCall(range, body()) } -> Nullness.NOT_NULL - else -> Nullness.UNKNOWN + data class FromDelegate(internal val value: Nullness, internal val delegateCalls: List) : MethodReturnInferenceResult { + override fun getNullness(method: PsiMethod, body: () -> PsiCodeBlock): Nullness { + if (value == Nullness.NULLABLE) { + return if (InferenceFromSourceUtil.suppressNullable(method)) Nullness.UNKNOWN else Nullness.NULLABLE + } + return when { + delegateCalls.all { range -> isNotNullCall(range, body()) } -> Nullness.NOT_NULL + else -> Nullness.UNKNOWN + } + } + + override fun getMutability(method: PsiMethod, body: () -> PsiCodeBlock): Mutability { + if (value == Nullness.NOT_NULL) { + return Mutability.UNKNOWN + } + return delegateCalls.stream().map { range -> getDelegateMutability(range, body()) }.reduce(Mutability::union).orElse( + Mutability.UNKNOWN) + } + + private fun getDelegateMutability(delegate: ExpressionRange, body: PsiCodeBlock): Mutability { + val call = delegate.restoreExpression(body) as PsiMethodCallExpression + val target = call.resolveMethod() + return when { + target == null -> Mutability.UNKNOWN + ClassUtils.isImmutable(target.returnType) -> Mutability.UNMODIFIABLE + else -> Mutability.getMutability(target) + } } private fun isNotNullCall(delegate: ExpressionRange, body: PsiCodeBlock): Boolean { @@ -118,7 +143,7 @@ interface NullityInferenceResult { } data class MethodData( - val nullity: NullityInferenceResult?, + val methodReturn: MethodReturnInferenceResult?, val purity: PurityInferenceResult?, val contracts: List, val notNullParameters: BitSet, diff --git a/java/java-tests/testData/inspection/dataFlow/fixture/MutabilityInferred.java b/java/java-tests/testData/inspection/dataFlow/fixture/MutabilityInferred.java new file mode 100644 index 000000000000..5be834fe27bc --- /dev/null +++ b/java/java-tests/testData/inspection/dataFlow/fixture/MutabilityInferred.java @@ -0,0 +1,37 @@ +import java.util.*; +import org.jetbrains.annotations.*; + +public class MutabilityInferred { + @Nullable + static List getListNullOrImmutable(int x) { + if(x == 0) return null; + return Collections.singletonList("foo"); + } + + static List getListImmutable(int x) { + if(x == 0) return Collections.singletonList("bar"); + return Collections.singletonList("foo"); + } + + static List getListImmutableOrMutable(int x) { + if(x == 0) return new ArrayList<>(); + return Collections.singletonList("foo"); + } + + static native List getListUnknown(int x); + + @Nullable + static List getListVar(boolean b) { + List list = getListUnknown(0); + if (list != null) return list; + if (b) return Collections.singletonList("xyz"); + return null; + } + + void test() { + getListNullOrImmutable(0).add("a"); + getListImmutable(0).add("b"); + getListImmutableOrMutable(0).add("c"); + getListVar(false).add("d"); + } +} diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection8Test.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection8Test.java index 20efcdf9a4b4..930afe6ea8e8 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection8Test.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/DataFlowInspection8Test.java @@ -213,9 +213,8 @@ public class DataFlowInspection8Test extends DataFlowInspectionTestCase { doTest(); } - public void testMutabilityJdk() { - doTest(); - } + public void testMutabilityJdk() { doTest(); } + public void testMutabilityInferred() { doTest(); } public void testPrimitiveGetters() { doTest(); } public void testUnknownOnStack() { doTest(); } diff --git a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ClassUtils.java b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ClassUtils.java index 7757331531d0..3694477f3776 100644 --- a/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ClassUtils.java +++ b/plugins/InspectionGadgets/InspectionGadgetsAnalysis/src/com/siyeh/ig/psiutils/ClassUtils.java @@ -116,7 +116,8 @@ public class ClassUtils { return integralTypes.contains(type); } - public static boolean isImmutable(PsiType type) { + @Contract("null -> false") + public static boolean isImmutable(@Nullable PsiType type) { if (TypeConversionUtil.isPrimitiveAndNotNull(type)) { return true; } @@ -124,7 +125,8 @@ public class ClassUtils { if (aClass == null) { return false; } - if (immutableTypes.contains(aClass.getQualifiedName())) { + String qualifiedName = aClass.getQualifiedName(); + if (immutableTypes.contains(qualifiedName) || (qualifiedName != null && qualifiedName.startsWith("com.google.common.collect.Immutable"))) { return true; } return JCiPUtil.isImmutable(aClass);