diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisConverter.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisConverter.java index c0e4fbc0aefb..812bfe7dbb8f 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisConverter.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisConverter.java @@ -43,14 +43,22 @@ public class BytecodeAnalysisConverter { * Returns null if conversion is impossible (something is not resolvable). */ @Nullable - public static EKey psiKey(@NotNull PsiMethod psiMethod, @NotNull Direction direction) { + public static EKey psiKey(@NotNull PsiMember psiMethod, @NotNull Direction direction) { PsiClass psiClass = psiMethod.getContainingClass(); if (psiClass != null) { String className = descriptor(psiClass, 0, false); - String methodSig = methodSignature(psiMethod, psiClass); - if (className != null && methodSig != null) { - String methodName = psiMethod.getReturnType() == null ? "" : psiMethod.getName(); - return new EKey(new Member(className, methodName, methodSig), direction, true, false); + String name = psiMethod.getName(); + String sig; + if (psiMethod instanceof PsiMethod) { + sig = methodSignature((PsiMethod)psiMethod, psiClass); + if (((PsiMethod)psiMethod).isConstructor()) { + name = ""; + } + } else if (psiMethod instanceof PsiField) { + sig = descriptor(((PsiField)psiMethod).getType()); + } else return null; + if (className != null && sig != null && name != null) { + return new EKey(new Member(className, name, sig), direction, true, false); } } return null; diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ClassDataIndexer.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ClassDataIndexer.java index 78dcf0e3104c..9f3bf547b6c9 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ClassDataIndexer.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ClassDataIndexer.java @@ -55,7 +55,7 @@ public class ClassDataIndexer implements VirtualFileGist.GistCalculator MERGER = (eq1, eq2) -> eq1.equals(eq2) ? eq1 : new Equations(Collections.emptyList(), false); - private static final int VERSION = 11; // change when inference algorithm changes + private static final int VERSION = 12; // change when inference algorithm changes private static final int VERSION_MODIFIER = HardCodedPurity.AGGRESSIVE_HARDCODED_PURITY ? 1 : 0; private static final int FINAL_VERSION = VERSION * 2 + VERSION_MODIFIER; private static final VirtualFileGist> ourGist = GistManager.getInstance().newVirtualFileGist( @@ -175,6 +175,7 @@ public class ClassDataIndexer implements VirtualFileGist.GistCalculator equations = new HashMap<>(); registerVolatileFields(equations, classReader); + Set staticFinalFields = getStaticFinalFields(classReader); if ((classReader.getAccess() & Opcodes.ACC_ENUM) != 0) { // ordinal() method is final in java.lang.Enum, but for some reason referred on call sites using specific enum class @@ -185,370 +186,8 @@ public class ClassDataIndexer implements VirtualFileGist.GistCalculator processMethod(final MethodNode methodNode, boolean jsr, Member method, boolean stable) { - ProgressManager.checkCanceled(); - final Type[] argumentTypes = Type.getArgumentTypes(methodNode.desc); - final Type resultType = Type.getReturnType(methodNode.desc); - final boolean isReferenceResult = ASMUtils.isReferenceType(resultType); - final boolean isBooleanResult = ASMUtils.isBooleanType(resultType); - final boolean isInterestingResult = isReferenceResult || isBooleanResult; - - List equations = new ArrayList<>(); - ContainerUtil.addIfNotNull(equations, PurityAnalysis.analyze(method, methodNode, stable)); - - try { - final ControlFlowGraph graph = ControlFlowGraph.build(className, methodNode, jsr); - if (graph.transitions.length > 0) { - final DFSTree dfs = DFSTree.build(graph.transitions, graph.edgeCount); - boolean branching = !dfs.back.isEmpty(); - if (!branching) { - for (int[] transition : graph.transitions) { - if (transition != null && transition.length > 1) { - branching = true; - break; - } - } - } - if (branching) { - RichControlFlow richControlFlow = new RichControlFlow(graph, dfs); - if (richControlFlow.reducible()) { - NegationAnalysis negated = tryNegation(method, argumentTypes, graph, isBooleanResult, dfs, jsr); - processBranchingMethod(method, methodNode, richControlFlow, argumentTypes, resultType, stable, jsr, equations, negated); - return equations; - } - LOG.debug(method + ": CFG is not reducible"); - } - // simple - else { - processNonBranchingMethod(method, argumentTypes, graph, resultType, stable, equations); - return equations; - } - } - // We can visit here if method body is absent (e.g. native method) - // Make sure to preserve hardcoded purity, if any. - equations.addAll(topEquations(method, argumentTypes, isReferenceResult, isInterestingResult, stable)); - return equations; - } - catch (ProcessCanceledException e) { - throw e; - } - catch (TooComplexException e) { - LOG.debug(method + " in " + presentableUrl + " is too complex for bytecode analysis"); - return topEquations(method, argumentTypes, isReferenceResult, isInterestingResult, stable); - } - catch (Throwable e) { - // incorrect bytecode may result in Runtime exceptions during analysis - // so here we suppose that exception is due to incorrect bytecode - LOG.debug("Unexpected Error during processing of " + method + " in " + presentableUrl, e); - return topEquations(method, argumentTypes, isReferenceResult, isInterestingResult, stable); - } - } - - private NegationAnalysis tryNegation(final Member method, - final Type[] argumentTypes, - final ControlFlowGraph graph, - final boolean isBooleanResult, - final DFSTree dfs, - final boolean jsr) throws AnalyzerException { - - class Util { - boolean isMethodCall(int opCode) { - return opCode == Opcodes.INVOKESTATIC || - opCode == Opcodes.INVOKESPECIAL || - opCode == Opcodes.INVOKEVIRTUAL || - opCode == Opcodes.INVOKEINTERFACE; - } - - boolean singleIfBranch() { - int branch = 0; - - for (int i = 0; i < graph.transitions.length; i++) { - int[] transition = graph.transitions[i]; - if (transition.length == 2) { - branch++; - int opCode = graph.methodNode.instructions.get(i).getOpcode(); - boolean isIfInsn = opCode == Opcodes.IFEQ || opCode == Opcodes.IFNE; - if (!isIfInsn) { - return false; - } - } - if (branch > 1) - return false; - } - return branch == 1; - } - - boolean singleMethodCall() { - int callCount = 0; - for (int i = 0; i < graph.transitions.length; i++) { - if (isMethodCall(graph.methodNode.instructions.get(i).getOpcode())) { - callCount++; - if (callCount > 1) { - return false; - } - } - } - return callCount == 1; - } - - public boolean booleanConstResult() { - try { - final boolean[] origins = - OriginsAnalysis.resultOrigins( - leakingParametersAndFrames(method, graph.methodNode, argumentTypes, jsr).frames, - graph.methodNode.instructions, - graph); - - for (int i = 0; i < origins.length; i++) { - if (origins[i]) { - int opCode = graph.methodNode.instructions.get(i).getOpcode(); - boolean isBooleanConst = opCode == Opcodes.ICONST_0 || opCode == Opcodes.ICONST_1; - if (!isBooleanConst) { - return false; - } - } - } - - return true; - } - catch (AnalyzerException ignore) { - } - return false; - } - } - - if (graph.methodNode.instructions.size() < 20 && isBooleanResult && dfs.back.isEmpty() && !jsr) { - Util util = new Util(); - if (util.singleIfBranch() && util.singleMethodCall() && util.booleanConstResult()) { - NegationAnalysis analyzer = new NegationAnalysis(method, graph); - try { - analyzer.analyze(); - return analyzer; - } - catch (NegationAnalysisFailedException ignore) { - return null; - } - } - } - - return null; - } - - private void processBranchingMethod(final Member method, - final MethodNode methodNode, - final RichControlFlow richControlFlow, - Type[] argumentTypes, - Type resultType, - final boolean stable, - boolean jsr, - List result, - NegationAnalysis negatedAnalysis) throws AnalyzerException { - final boolean isReferenceResult = ASMUtils.isReferenceType(resultType); - final boolean isBooleanResult = ASMUtils.isBooleanType(resultType); - boolean isInterestingResult = isBooleanResult || isReferenceResult; - - final LeakingParameters leakingParametersAndFrames = leakingParametersAndFrames(method, methodNode, argumentTypes, jsr); - - boolean[] leakingParameters = leakingParametersAndFrames.parameters; - boolean[] leakingNullableParameters = leakingParametersAndFrames.nullableParameters; - - final boolean[] origins = - OriginsAnalysis.resultOrigins(leakingParametersAndFrames.frames, methodNode.instructions, richControlFlow.controlFlow); - - Equation outEquation = - isInterestingResult ? - new InOutAnalysis(richControlFlow, Out, origins, stable, sharedPendingStates).analyze() : - null; - - if (isReferenceResult) { - result.add(outEquation); - result.add(new Equation(new EKey(method, NullableOut, stable), NullableMethodAnalysis.analyze(methodNode, origins, jsr))); - } - final boolean shouldInferNonTrivialFailingContracts; - final Equation throwEquation; - if (methodNode.name.equals("")) { - // Do not infer failing contracts for constructors - shouldInferNonTrivialFailingContracts = false; - throwEquation = new Equation(new EKey(method, Throw, stable), Value.Top); - } - else { - final InThrowAnalysis inThrowAnalysis = new InThrowAnalysis(richControlFlow, Throw, origins, stable, sharedPendingStates); - throwEquation = inThrowAnalysis.analyze(); - if (!throwEquation.result.equals(Value.Top)) { - result.add(throwEquation); - } - shouldInferNonTrivialFailingContracts = !inThrowAnalysis.myHasNonTrivialReturn; - } - - boolean withCycle = !richControlFlow.dfsTree.back.isEmpty(); - if (argumentTypes.length > 50 && withCycle) { - // IDEA-137443 - do not analyze very complex methods - return; - } - - final IntFunction>> inOuts = - index -> val -> { - if (isBooleanResult && negatedAnalysis != null) { - return Stream.of(negatedAnalysis.contractEquation(index, val, stable)); - } - Stream.Builder builder = Stream.builder(); - try { - if (isInterestingResult) { - builder.add(new InOutAnalysis(richControlFlow, new InOut(index, val), origins, stable, sharedPendingStates).analyze()); - } - if (shouldInferNonTrivialFailingContracts) { - InThrow direction = new InThrow(index, val); - if (throwEquation.result.equals(Value.Fail)) { - builder.add(new Equation(new EKey(method, direction, stable), Value.Fail)); - } - else { - builder.add(new InThrowAnalysis(richControlFlow, direction, origins, stable, sharedPendingStates).analyze()); - } - } - } - catch (AnalyzerException e) { - throw new RuntimeException("Analyzer error", e); - } - return builder.build(); - }; - // arguments and contract clauses - for (int i = 0; i < argumentTypes.length; i++) { - boolean notNullParam = false; - - if (ASMUtils.isReferenceType(argumentTypes[i])) { - boolean possibleNPE = false; - if (leakingParameters[i]) { - NonNullInAnalysis notNullInAnalysis = - new NonNullInAnalysis(richControlFlow, new In(i, false), stable, sharedPendingActions, sharedResults); - Equation notNullParamEquation = notNullInAnalysis.analyze(); - possibleNPE = notNullInAnalysis.possibleNPE; - notNullParam = notNullParamEquation.result.equals(Value.NotNull); - result.add(notNullParamEquation); - } - else { - // parameter is not leaking, so it is definitely NOT @NotNull - result.add(new Equation(new EKey(method, new In(i, false), stable), Value.Top)); - } - - if (leakingNullableParameters[i]) { - if (notNullParam || possibleNPE) { - result.add(new Equation(new EKey(method, new In(i, true), stable), Value.Top)); - } - else { - result.add(new NullableInAnalysis(richControlFlow, new In(i, true), stable, sharedPendingStates).analyze()); - } - } - else { - result.add(new Equation(new EKey(method, new In(i, true), stable), Value.Null)); - } - - if (isInterestingResult) { - if (!leakingParameters[i]) { - // parameter is not leaking, so a contract is the same as for the whole method - result.add(new Equation(new EKey(method, new InOut(i, Value.Null), stable), outEquation.result)); - result.add(new Equation(new EKey(method, new InOut(i, Value.NotNull), stable), outEquation.result)); - continue; - } - if (notNullParam) { - // @NotNull, like "null->fail" - result.add(new Equation(new EKey(method, new InOut(i, Value.Null), stable), Value.Bot)); - result.add(new Equation(new EKey(method, new InOut(i, Value.NotNull), stable), outEquation.result)); - continue; - } - } - } - Value.typeValues(argumentTypes[i]).flatMap(inOuts.apply(i)).forEach(result::add); - } - } - - private void processNonBranchingMethod(Member method, - Type[] argumentTypes, - ControlFlowGraph graph, - Type returnType, - boolean stable, - List result) throws AnalyzerException { - CombinedAnalysis analyzer = new CombinedAnalysis(method, graph); - analyzer.analyze(); - ContainerUtil.addIfNotNull(result, analyzer.outContractEquation(stable)); - ContainerUtil.addIfNotNull(result, analyzer.failEquation(stable)); - if (ASMUtils.isReferenceType(returnType)) { - result.add(analyzer.nullableResultEquation(stable)); - } - EntryStream.of(argumentTypes).forKeyValue((i, argType) -> { - if (ASMUtils.isReferenceType(argType)) { - result.add(analyzer.notNullParamEquation(i, stable)); - result.add(analyzer.nullableParamEquation(i, stable)); - } - Value.typeValues(argType) - .flatMap(val -> Stream.of(analyzer.contractEquation(i, val, stable), analyzer.failEquation(i, val, stable))) - .filter(Objects::nonNull) - .forEach(result::add); - }); - } - - private List topEquations(Member method, - Type[] argumentTypes, - boolean isReferenceResult, - boolean isInterestingResult, - boolean stable) { - // 4 = @NotNull parameter, @Nullable parameter, null -> ..., !null -> ... - List result = new ArrayList<>(argumentTypes.length * 4 + 2); - if (isReferenceResult) { - result.add(new Equation(new EKey(method, Out, stable), Value.Top)); - result.add(new Equation(new EKey(method, NullableOut, stable), Value.Bot)); - } - for (int i = 0; i < argumentTypes.length; i++) { - if (ASMUtils.isReferenceType(argumentTypes[i])) { - result.add(new Equation(new EKey(method, new In(i, false), stable), Value.Top)); - result.add(new Equation(new EKey(method, new In(i, true), stable), Value.Top)); - if (isInterestingResult) { - result.add(new Equation(new EKey(method, new InOut(i, Value.Null), stable), Value.Top)); - result.add(new Equation(new EKey(method, new InOut(i, Value.NotNull), stable), Value.Top)); - } - } - } - return result; - } - - @NotNull - private LeakingParameters leakingParametersAndFrames(Member method, MethodNode methodNode, Type[] argumentTypes, boolean jsr) - throws AnalyzerException { - return argumentTypes.length < 32 ? - LeakingParameters.buildFast(method.internalClassName, methodNode, jsr) : - LeakingParameters.build(method.internalClassName, methodNode, jsr); - } - }, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + classReader.accept( + new MethodAnalysisVisitor(equations, presentableUrl, sharedPendingStates, sharedPendingActions, sharedResults, staticFinalFields), ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); return equations; } @@ -566,6 +205,22 @@ public class ClassDataIndexer implements VirtualFileGist.GistCalculator getStaticFinalFields(ClassReader classReader) { + Set staticFields = new HashSet<>(); + classReader.accept(new ClassVisitor(Opcodes.API_VERSION) { + @Override + public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { + int modifiers = Opcodes.ACC_STATIC | Opcodes.ACC_FINAL; + if ((access & modifiers) == modifiers && (access & (Opcodes.ACC_ENUM | Opcodes.ACC_SYNTHETIC)) == 0 && + (desc.startsWith("L") || desc.startsWith("["))) { + staticFields.add(new Member(classReader.getClassName(), name, desc)); + } + return null; + } + }, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE); + return staticFields; + } + @NotNull static List getEquations(GlobalSearchScope scope, HMember key) { return ContainerUtil.mapNotNull(FileBasedIndex.getInstance().getContainingFiles(BytecodeAnalysisIndex.NAME, key, scope), @@ -597,4 +252,397 @@ public class ClassDataIndexer implements VirtualFileGist.GistCalculator myEquations; + private final String myPresentableUrl; + private final State[] mySharedPendingStates; + private final PendingAction[] mySharedPendingActions; + private final PResults.PResult[] mySharedResults; + private final Set myStaticFinalFields; + + private MethodAnalysisVisitor(Map equations, + String presentableUrl, + State[] sharedPendingStates, + PendingAction[] sharedPendingActions, + PResults.PResult[] sharedResults, Set staticFinalFields) { + myEquations = equations; + myPresentableUrl = presentableUrl; + mySharedPendingStates = sharedPendingStates; + mySharedPendingActions = sharedPendingActions; + mySharedResults = sharedResults; + myStaticFinalFields = staticFinalFields; + } + + @Override + protected MethodVisitor visitMethod(final MethodNode node, Member method, final EKey key) { + return new MethodVisitor(Opcodes.API_VERSION, node) { + private boolean jsr; + + @Override + public void visitJumpInsn(int opcode, Label label) { + if (opcode == Opcodes.JSR) { + jsr = true; + } + super.visitJumpInsn(opcode, label); + } + + @Override + public void visitEnd() { + super.visitEnd(); + myEquations.put(key, convertEquations(key, processMethod(node, jsr, method, key.stable))); + } + }; + } + + /** + * Facade for analysis, it invokes specialized analyses for branching/non-branching methods. + * + * @param methodNode asm node for method + * @param jsr whether a method has jsr instruction + * @param method a method descriptor + * @param stable whether a method is stable (final or declared in a final class) + */ + private List processMethod(final MethodNode methodNode, boolean jsr, Member method, boolean stable) { + ProgressManager.checkCanceled(); + final Type[] argumentTypes = Type.getArgumentTypes(methodNode.desc); + final Type resultType = Type.getReturnType(methodNode.desc); + final boolean isReferenceResult = ASMUtils.isReferenceType(resultType); + final boolean isBooleanResult = ASMUtils.isBooleanType(resultType); + final boolean isInterestingResult = isReferenceResult || isBooleanResult; + + List equations = new ArrayList<>(); + ContainerUtil.addIfNotNull(equations, PurityAnalysis.analyze(method, methodNode, stable)); + + try { + final ControlFlowGraph graph = ControlFlowGraph.build(className, methodNode, jsr); + if (graph.transitions.length > 0) { + final DFSTree dfs = DFSTree.build(graph.transitions, graph.edgeCount); + boolean branching = !dfs.back.isEmpty(); + if (!branching) { + for (int[] transition : graph.transitions) { + if (transition != null && transition.length > 1) { + branching = true; + break; + } + } + } + if (branching) { + RichControlFlow richControlFlow = new RichControlFlow(graph, dfs); + if (richControlFlow.reducible()) { + NegationAnalysis negated = tryNegation(method, argumentTypes, graph, isBooleanResult, dfs, jsr); + processBranchingMethod(method, methodNode, richControlFlow, argumentTypes, resultType, stable, jsr, equations, negated); + return equations; + } + LOG.debug(method + ": CFG is not reducible"); + } + // simple + else { + processNonBranchingMethod(method, argumentTypes, graph, resultType, stable, equations); + return equations; + } + } + // We can visit here if method body is absent (e.g. native method) + // Make sure to preserve hardcoded purity, if any. + equations.addAll(topEquations(method, argumentTypes, isReferenceResult, isInterestingResult, stable)); + return equations; + } + catch (ProcessCanceledException e) { + throw e; + } + catch (TooComplexException e) { + LOG.debug(method + " in " + myPresentableUrl + " is too complex for bytecode analysis"); + return topEquations(method, argumentTypes, isReferenceResult, isInterestingResult, stable); + } + catch (Throwable e) { + // incorrect bytecode may result in Runtime exceptions during analysis + // so here we suppose that exception is due to incorrect bytecode + LOG.debug("Unexpected Error during processing of " + method + " in " + myPresentableUrl, e); + return topEquations(method, argumentTypes, isReferenceResult, isInterestingResult, stable); + } + } + + private static NegationAnalysis tryNegation(final Member method, + final Type[] argumentTypes, + final ControlFlowGraph graph, + final boolean isBooleanResult, + final DFSTree dfs, + final boolean jsr) throws AnalyzerException { + + class Util { + boolean isMethodCall(int opCode) { + return opCode == Opcodes.INVOKESTATIC || + opCode == Opcodes.INVOKESPECIAL || + opCode == Opcodes.INVOKEVIRTUAL || + opCode == Opcodes.INVOKEINTERFACE; + } + + boolean singleIfBranch() { + int branch = 0; + + for (int i = 0; i < graph.transitions.length; i++) { + int[] transition = graph.transitions[i]; + if (transition.length == 2) { + branch++; + int opCode = graph.methodNode.instructions.get(i).getOpcode(); + boolean isIfInsn = opCode == Opcodes.IFEQ || opCode == Opcodes.IFNE; + if (!isIfInsn) { + return false; + } + } + if (branch > 1) + return false; + } + return branch == 1; + } + + boolean singleMethodCall() { + int callCount = 0; + for (int i = 0; i < graph.transitions.length; i++) { + if (isMethodCall(graph.methodNode.instructions.get(i).getOpcode())) { + callCount++; + if (callCount > 1) { + return false; + } + } + } + return callCount == 1; + } + + public boolean booleanConstResult() { + try { + final boolean[] origins = + OriginsAnalysis.resultOrigins( + leakingParametersAndFrames(method, graph.methodNode, argumentTypes, jsr).frames, + graph.methodNode.instructions, + graph); + + for (int i = 0; i < origins.length; i++) { + if (origins[i]) { + int opCode = graph.methodNode.instructions.get(i).getOpcode(); + boolean isBooleanConst = opCode == Opcodes.ICONST_0 || opCode == Opcodes.ICONST_1; + if (!isBooleanConst) { + return false; + } + } + } + + return true; + } + catch (AnalyzerException ignore) { + } + return false; + } + } + + if (graph.methodNode.instructions.size() < 20 && isBooleanResult && dfs.back.isEmpty() && !jsr) { + Util util = new Util(); + if (util.singleIfBranch() && util.singleMethodCall() && util.booleanConstResult()) { + NegationAnalysis analyzer = new NegationAnalysis(method, graph); + try { + analyzer.analyze(); + return analyzer; + } + catch (NegationAnalysisFailedException ignore) { + return null; + } + } + } + + return null; + } + + private void processBranchingMethod(final Member method, + final MethodNode methodNode, + final RichControlFlow richControlFlow, + Type[] argumentTypes, + Type resultType, + final boolean stable, + boolean jsr, + List result, + NegationAnalysis negatedAnalysis) throws AnalyzerException { + final boolean isReferenceResult = ASMUtils.isReferenceType(resultType); + final boolean isBooleanResult = ASMUtils.isBooleanType(resultType); + boolean isInterestingResult = isBooleanResult || isReferenceResult; + + final LeakingParameters leakingParametersAndFrames = leakingParametersAndFrames(method, methodNode, argumentTypes, jsr); + + boolean[] leakingParameters = leakingParametersAndFrames.parameters; + boolean[] leakingNullableParameters = leakingParametersAndFrames.nullableParameters; + + final boolean[] origins = + OriginsAnalysis.resultOrigins(leakingParametersAndFrames.frames, methodNode.instructions, richControlFlow.controlFlow); + + Equation outEquation = + isInterestingResult ? + new InOutAnalysis(richControlFlow, Out, origins, stable, mySharedPendingStates).analyze() : + null; + + if (isReferenceResult) { + result.add(outEquation); + result.add(new Equation(new EKey(method, NullableOut, stable), NullableMethodAnalysis.analyze(methodNode, origins, jsr))); + } + final boolean shouldInferNonTrivialFailingContracts; + final Equation throwEquation; + if (methodNode.name.equals("")) { + // Do not infer failing contracts for constructors + shouldInferNonTrivialFailingContracts = false; + throwEquation = new Equation(new EKey(method, Throw, stable), Value.Top); + } + else { + final InThrowAnalysis inThrowAnalysis = new InThrowAnalysis(richControlFlow, Throw, origins, stable, mySharedPendingStates); + throwEquation = inThrowAnalysis.analyze(); + if (!throwEquation.result.equals(Value.Top)) { + result.add(throwEquation); + } + shouldInferNonTrivialFailingContracts = !inThrowAnalysis.myHasNonTrivialReturn; + } + + boolean withCycle = !richControlFlow.dfsTree.back.isEmpty(); + if (argumentTypes.length > 50 && withCycle) { + // IDEA-137443 - do not analyze very complex methods + return; + } + + final IntFunction>> inOuts = + index -> val -> { + if (isBooleanResult && negatedAnalysis != null) { + return Stream.of(negatedAnalysis.contractEquation(index, val, stable)); + } + Stream.Builder builder = Stream.builder(); + try { + if (isInterestingResult) { + builder.add(new InOutAnalysis(richControlFlow, new InOut(index, val), origins, stable, mySharedPendingStates).analyze()); + } + if (shouldInferNonTrivialFailingContracts) { + InThrow direction = new InThrow(index, val); + if (throwEquation.result.equals(Value.Fail)) { + builder.add(new Equation(new EKey(method, direction, stable), Value.Fail)); + } + else { + builder.add(new InThrowAnalysis(richControlFlow, direction, origins, stable, mySharedPendingStates).analyze()); + } + } + } + catch (AnalyzerException e) { + throw new RuntimeException("Analyzer error", e); + } + return builder.build(); + }; + // arguments and contract clauses + for (int i = 0; i < argumentTypes.length; i++) { + boolean notNullParam = false; + + if (ASMUtils.isReferenceType(argumentTypes[i])) { + boolean possibleNPE = false; + if (leakingParameters[i]) { + NonNullInAnalysis notNullInAnalysis = + new NonNullInAnalysis(richControlFlow, new In(i, false), stable, mySharedPendingActions, mySharedResults); + Equation notNullParamEquation = notNullInAnalysis.analyze(); + possibleNPE = notNullInAnalysis.possibleNPE; + notNullParam = notNullParamEquation.result.equals(Value.NotNull); + result.add(notNullParamEquation); + } + else { + // parameter is not leaking, so it is definitely NOT @NotNull + result.add(new Equation(new EKey(method, new In(i, false), stable), Value.Top)); + } + + if (leakingNullableParameters[i]) { + if (notNullParam || possibleNPE) { + result.add(new Equation(new EKey(method, new In(i, true), stable), Value.Top)); + } + else { + result.add(new NullableInAnalysis(richControlFlow, new In(i, true), stable, mySharedPendingStates).analyze()); + } + } + else { + result.add(new Equation(new EKey(method, new In(i, true), stable), Value.Null)); + } + + if (isInterestingResult) { + if (!leakingParameters[i]) { + // parameter is not leaking, so a contract is the same as for the whole method + result.add(new Equation(new EKey(method, new InOut(i, Value.Null), stable), outEquation.result)); + result.add(new Equation(new EKey(method, new InOut(i, Value.NotNull), stable), outEquation.result)); + continue; + } + if (notNullParam) { + // @NotNull, like "null->fail" + result.add(new Equation(new EKey(method, new InOut(i, Value.Null), stable), Value.Bot)); + result.add(new Equation(new EKey(method, new InOut(i, Value.NotNull), stable), outEquation.result)); + continue; + } + } + } + Value.typeValues(argumentTypes[i]).flatMap(inOuts.apply(i)).forEach(result::add); + } + } + + private void processNonBranchingMethod(Member method, + Type[] argumentTypes, + ControlFlowGraph graph, + Type returnType, + boolean stable, + List result) throws AnalyzerException { + Set fieldsToTrack = method.methodName.equals("") ? myStaticFinalFields : Collections.emptySet(); + CombinedAnalysis analyzer = new CombinedAnalysis(method, graph, fieldsToTrack); + analyzer.analyze(); + ContainerUtil.addIfNotNull(result, analyzer.outContractEquation(stable)); + ContainerUtil.addIfNotNull(result, analyzer.failEquation(stable)); + storeStaticFieldEquations(analyzer); + if (ASMUtils.isReferenceType(returnType)) { + result.add(analyzer.nullableResultEquation(stable)); + } + EntryStream.of(argumentTypes).forKeyValue((i, argType) -> { + if (ASMUtils.isReferenceType(argType)) { + result.add(analyzer.notNullParamEquation(i, stable)); + result.add(analyzer.nullableParamEquation(i, stable)); + } + Value.typeValues(argType) + .flatMap(val -> Stream.of(analyzer.contractEquation(i, val, stable), analyzer.failEquation(i, val, stable))) + .filter(Objects::nonNull) + .forEach(result::add); + }); + } + + private void storeStaticFieldEquations(CombinedAnalysis analyzer) { + for (Equation equation : analyzer.staticFieldEquations()) { + myEquations.put(equation.key, + new Equations(Collections.singletonList(new DirectionResultPair(equation.key.dirKey, equation.result)), true)); + } + } + + private static List topEquations(Member method, + Type[] argumentTypes, + boolean isReferenceResult, + boolean isInterestingResult, + boolean stable) { + // 4 = @NotNull parameter, @Nullable parameter, null -> ..., !null -> ... + List result = new ArrayList<>(argumentTypes.length * 4 + 2); + if (isReferenceResult) { + result.add(new Equation(new EKey(method, Out, stable), Value.Top)); + result.add(new Equation(new EKey(method, NullableOut, stable), Value.Bot)); + } + for (int i = 0; i < argumentTypes.length; i++) { + if (ASMUtils.isReferenceType(argumentTypes[i])) { + result.add(new Equation(new EKey(method, new In(i, false), stable), Value.Top)); + result.add(new Equation(new EKey(method, new In(i, true), stable), Value.Top)); + if (isInterestingResult) { + result.add(new Equation(new EKey(method, new InOut(i, Value.Null), stable), Value.Top)); + result.add(new Equation(new EKey(method, new InOut(i, Value.NotNull), stable), Value.Top)); + } + } + } + return result; + } + + @NotNull + private static LeakingParameters leakingParametersAndFrames(Member method, MethodNode methodNode, Type[] argumentTypes, boolean jsr) + throws AnalyzerException { + return argumentTypes.length < 32 ? + LeakingParameters.buildFast(method.internalClassName, methodNode, jsr) : + LeakingParameters.build(method.internalClassName, methodNode, jsr); + } + } } \ No newline at end of file diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Combined.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Combined.java index aae4ccdb831e..c0643176c58b 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Combined.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Combined.java @@ -4,6 +4,8 @@ package com.intellij.codeInspection.bytecodeAnalysis; import com.intellij.codeInspection.bytecodeAnalysis.asm.ASMUtils; import com.intellij.codeInspection.bytecodeAnalysis.asm.ControlFlowGraph; import com.intellij.util.SingletonSet; +import one.util.streamex.EntryStream; +import one.util.streamex.StreamEx; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.org.objectweb.asm.Handle; @@ -143,11 +145,11 @@ final class CombinedAnalysis { private boolean exception; private final MethodNode methodNode; - CombinedAnalysis(Member method, ControlFlowGraph controlFlow) { + CombinedAnalysis(Member method, ControlFlowGraph controlFlow, Set staticFields) { this.method = method; this.controlFlow = controlFlow; methodNode = controlFlow.methodNode; - interpreter = new CombinedInterpreter(methodNode.instructions, Type.getArgumentTypes(methodNode.desc).length); + interpreter = new CombinedInterpreter(methodNode.instructions, Type.getArgumentTypes(methodNode.desc).length, staticFields); } final void analyze() throws AnalyzerException { @@ -312,7 +314,20 @@ final class CombinedAnalysis { @Nullable final Equation outContractEquation(boolean stable) { - final EKey key = new EKey(method, Out, stable); + return outEquation(exception, method, returnValue, stable); + } + + final List staticFieldEquations() { + return EntryStream.of(interpreter.staticFields) + .removeValues(v -> v == BasicValue.UNINITIALIZED_VALUE) + .mapKeyValue((field, value) -> outEquation(exception, field, value, true)) + .nonNull() + .toList(); + } + + @Nullable + private static Equation outEquation(boolean exception, Member member, BasicValue returnValue, boolean stable) { + final EKey key = new EKey(member, Out, stable); final Result result; if (exception) { result = Value.Bot; @@ -405,15 +420,20 @@ final class CombinedInterpreter extends BasicInterpreter { final List calls = new ArrayList<>(); + final Map staticFields; + private final InsnList insns; - CombinedInterpreter(InsnList insns, int arity) { + CombinedInterpreter(InsnList insns, + int arity, + Set staticFields) { super(Opcodes.API_VERSION); dereferencedParams = new boolean[arity]; notNullableParams = new boolean[arity]; parameterFlow = new Set[arity]; this.insns = insns; dereferencedValues = new boolean[insns.size()]; + this.staticFields = StreamEx.of(staticFields).cross(BasicValue.UNINITIALIZED_VALUE).toMap(); } private int insnIndex(AbstractInsnNode insn) { @@ -473,6 +493,13 @@ final class CombinedInterpreter extends BasicInterpreter { dereferencedValues[((Trackable)value).getOriginInsnIndex()] = true; } return track(origin, super.unaryOperation(insn, value)); + case PUTSTATIC: + if (!staticFields.isEmpty()) { + FieldInsnNode node = (FieldInsnNode)insn; + Member field = new Member(node.owner, node.name, node.desc); + staticFields.computeIfPresent(field, (f, v) -> value); + } + break; case CHECKCAST: if (value instanceof NthParamValue) { return new NthParamValue(Type.getObjectType(((TypeInsnNode)insn).desc), ((NthParamValue)value).n); diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ProjectBytecodeAnalysis.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ProjectBytecodeAnalysis.java index 69005a7697eb..7dacc1f05253 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ProjectBytecodeAnalysis.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ProjectBytecodeAnalysis.java @@ -121,6 +121,15 @@ public class ProjectBytecodeAnalysis { ParameterAnnotations parameterAnnotations = loadParameterAnnotations(primaryKey); return toPsi(parameterAnnotations); } + else if (listOwner instanceof PsiField && listOwner.hasModifierProperty(PsiModifier.STATIC)) { + Solver outSolver = new Solver(new ELattice<>(Value.Bot, Value.Top), Value.Top); + collectEquations(Collections.singletonList(primaryKey), outSolver); + Map solutions = outSolver.solve(); + Value value = solutions.get(primaryKey); + if (value == Value.NotNull) { + return new PsiAnnotation[]{getNotNullAnnotation()}; + } + } return PsiAnnotation.EMPTY_ARRAY; } catch (EquationsLimitException e) { @@ -213,22 +222,24 @@ public class ProjectBytecodeAnalysis { @Nullable public EKey getKey(@NotNull PsiModifierListOwner owner, MessageDigest md) { LOG.assertTrue(owner instanceof PsiCompiledElement, owner); + EKey key = null; if (owner instanceof PsiMethod) { - EKey key = BytecodeAnalysisConverter.psiKey((PsiMethod)owner, Out); - return key == null ? null : myEquationProvider.adaptKey(key, md); + key = BytecodeAnalysisConverter.psiKey((PsiMethod)owner, Out); } - if (owner instanceof PsiParameter) { + else if (owner instanceof PsiField) { + key = BytecodeAnalysisConverter.psiKey((PsiField)owner, Out); + } + else if (owner instanceof PsiParameter) { PsiElement parent = owner.getParent(); if (parent instanceof PsiParameterList) { PsiElement gParent = parent.getParent(); if (gParent instanceof PsiMethod) { int index = ((PsiParameterList)parent).getParameterIndex((PsiParameter)owner); - EKey key = BytecodeAnalysisConverter.psiKey((PsiMethod)gParent, new In(index, false)); - return key == null ? null : myEquationProvider.adaptKey(key, md); + key = BytecodeAnalysisConverter.psiKey((PsiMethod)gParent, new In(index, false)); } } } - return null; + return key == null ? null : myEquationProvider.adaptKey(key, md); } /** @@ -326,7 +337,8 @@ public class ProjectBytecodeAnalysis { for (Equations equations : myEquationProvider.getEquations(curKey.member)) { stable &= equations.stable; Effects effects = (Effects)equations.find(curKey.getDirection()) - .orElseGet(() -> new Effects(DataValue.UnknownDataValue1, Effects.TOP_EFFECTS)); + .orElseGet(() -> new Effects(DataValue.UnknownDataValue1, + curKey.getDirection() == Volatile ? Collections.emptySet() : Effects.TOP_EFFECTS)); combined = combined == null ? effects : combined.combine(effects); } if (combined != null) { diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/awt/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/awt/annotations.xml index cd8aa069a31f..c7007b595150 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/awt/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/awt/annotations.xml @@ -495,9 +495,15 @@ + + + + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/lang/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/lang/annotations.xml index b43281f55ed4..9d72b5212d87 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/lang/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/lang/annotations.xml @@ -265,6 +265,12 @@ + + + + + + @@ -705,6 +711,612 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -717,6 +1329,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/lang/invoke/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/lang/invoke/annotations.xml index a53fa02baad0..2b8ef26f9f51 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/lang/invoke/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/lang/invoke/annotations.xml @@ -322,6 +322,9 @@ + + + @@ -869,6 +872,12 @@ + + + + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/sql/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/sql/annotations.xml index 9ee138d83a67..aad12cfa10bb 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/sql/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/sql/annotations.xml @@ -123,6 +123,9 @@ + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/util/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/util/annotations.xml index 127e15345c94..a08435a0954d 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/util/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/util/annotations.xml @@ -1447,6 +1447,15 @@ + + + + + + + + + @@ -2247,6 +2256,9 @@ + + + @@ -2262,6 +2274,9 @@ + + + @@ -2357,6 +2372,9 @@ + + + @@ -2523,6 +2541,9 @@ + + + @@ -3239,6 +3260,9 @@ + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/util/concurrent/locks/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/util/concurrent/locks/annotations.xml index fd8b8adfbd5f..bbc95ee36824 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/util/concurrent/locks/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/java/util/concurrent/locks/annotations.xml @@ -163,6 +163,9 @@ + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/annotations.xml index faa86f373b46..711f21fa705b 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/annotations.xml @@ -134,6 +134,9 @@ + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/functors/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/functors/annotations.xml index 468212d94da9..37ca99acd040 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/functors/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/functors/annotations.xml @@ -119,6 +119,9 @@ + + + @@ -163,6 +166,9 @@ + + + @@ -187,6 +193,9 @@ + + + @@ -233,6 +242,9 @@ + + + @@ -252,6 +264,9 @@ + + + @@ -268,6 +283,9 @@ + + + @@ -287,6 +305,9 @@ + + + @@ -329,6 +350,9 @@ + + + @@ -553,6 +577,9 @@ + + + @@ -613,6 +640,9 @@ + + + @@ -631,6 +661,9 @@ + + + @@ -663,6 +696,9 @@ + + + @@ -789,6 +825,9 @@ + + + @@ -896,6 +935,9 @@ + + + @@ -1064,6 +1106,9 @@ + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/iterators/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/iterators/annotations.xml index ce90c27e4fcf..36fb4edd417d 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/iterators/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/iterators/annotations.xml @@ -230,26 +230,41 @@ + + + + + + + + + + + + + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/map/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/map/annotations.xml index 5d3efe275b90..77fd3d7f74b0 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/map/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/collections/map/annotations.xml @@ -7,6 +7,9 @@ + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/annotations.xml index e5ed72532d70..099cb6fe7231 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/annotations.xml @@ -4,6 +4,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1685,9 +1742,27 @@ + + + + + + + + + + + + + + + + + + @@ -2101,6 +2176,21 @@ + + + + + + + + + + + + + + + @@ -2507,6 +2597,9 @@ + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/builder/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/builder/annotations.xml index 15f64be4950b..e0cbc4fad824 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/builder/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/builder/annotations.xml @@ -1191,6 +1191,21 @@ + + + + + + + + + + + + + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/math/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/math/annotations.xml index 95e8e2c0b17a..a8e5acd67cee 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/math/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/math/annotations.xml @@ -201,11 +201,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -722,11 +758,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -987,6 +1077,9 @@ + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/time/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/time/annotations.xml index e3f089584cde..fd295dc2adc0 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/time/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/commons/lang/time/annotations.xml @@ -357,6 +357,18 @@ + + + + + + + + + + + + @@ -393,9 +405,18 @@ + + + + + + + + + @@ -612,6 +633,12 @@ + + + + + + @@ -644,6 +671,9 @@ + + + @@ -682,6 +712,9 @@ + + + @@ -701,6 +734,9 @@ + + + @@ -720,6 +756,9 @@ + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/velocity/io/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/velocity/io/annotations.xml index 0cd67c1ed094..236072f242c1 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/velocity/io/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/velocity/io/annotations.xml @@ -1,4 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/velocity/runtime/parser/annotations.xml b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/velocity/runtime/parser/annotations.xml index 576532f1a791..6dba21c37175 100644 --- a/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/velocity/runtime/parser/annotations.xml +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/annotations/org/apache/velocity/runtime/parser/annotations.xml @@ -76,6 +76,9 @@ + + + @@ -126,6 +129,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -140,6 +173,9 @@ + + + diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/data/classes/bytecodeAnalysis/data/Test01.class b/java/java-tests/testData/codeInspection/bytecodeAnalysis/data/classes/bytecodeAnalysis/data/Test01.class index c19bd300069e..4ac10af46421 100644 Binary files a/java/java-tests/testData/codeInspection/bytecodeAnalysis/data/classes/bytecodeAnalysis/data/Test01.class and b/java/java-tests/testData/codeInspection/bytecodeAnalysis/data/classes/bytecodeAnalysis/data/Test01.class differ diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/data/classes/bytecodeAnalysis/data/TestField.class b/java/java-tests/testData/codeInspection/bytecodeAnalysis/data/classes/bytecodeAnalysis/data/TestField.class new file mode 100644 index 000000000000..5eab84b7b728 Binary files /dev/null and b/java/java-tests/testData/codeInspection/bytecodeAnalysis/data/classes/bytecodeAnalysis/data/TestField.class differ diff --git a/java/java-tests/testData/codeInspection/bytecodeAnalysis/data/src/data/TestField.java b/java/java-tests/testData/codeInspection/bytecodeAnalysis/data/src/data/TestField.java new file mode 100644 index 000000000000..105a8825560c --- /dev/null +++ b/java/java-tests/testData/codeInspection/bytecodeAnalysis/data/src/data/TestField.java @@ -0,0 +1,22 @@ +package bytecodeAnalysis.data; + +import bytecodeAnalysis.ExpectNotNull; +import bytecodeAnalysis.ExpectContract; + +public class TestField { + @ExpectNotNull + public static final Object O1 = new Object(); + @ExpectNotNull + public static final Object O2 = getObject(); + @ExpectNotNull + public static final Runnable O3 = () -> {}; + + public static Object O4 = new Object(); // non-final + public final Object O5 = new Object(); // non-static + + @ExpectNotNull + @ExpectContract(value="->new",pure=true) + private static Object getObject() { + return new Object(); + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/bytecodeAnalysis/BytecodeAnalysisIntegrationTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/bytecodeAnalysis/BytecodeAnalysisIntegrationTest.java index 5772d43b8ab1..49a2a2c42bd1 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/bytecodeAnalysis/BytecodeAnalysisIntegrationTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/bytecodeAnalysis/BytecodeAnalysisIntegrationTest.java @@ -134,6 +134,9 @@ public class BytecodeAnalysisIntegrationTest extends LightJavaCodeInsightFixture for (PsiMethod method : aClass.getMethods()) { checkMethodAnnotations(method, digest, diffs); } + for (PsiField field : aClass.getFields()) { + checkFieldAnnotations(field, digest, diffs); + } } } }; @@ -185,6 +188,17 @@ public class BytecodeAnalysisIntegrationTest extends LightJavaCodeInsightFixture } } + private void checkFieldAnnotations(PsiField field, MessageDigest digest, List diffs) { + if (ProjectBytecodeAnalysis.getInstance(getProject()).getKey(field, digest) == null) return; + String fieldKey = PsiFormatUtil.getExternalName(field, false, Integer.MAX_VALUE); + + String externalNotNullMethodAnnotation = findExternalAnnotation(field, AnnotationUtil.NOT_NULL) == null ? "-" : "@NotNull"; + String inferredNotNullMethodAnnotation = findInferredAnnotation(field, AnnotationUtil.NOT_NULL) == null ? "-" : "@NotNull"; + if (!externalNotNullMethodAnnotation.equals(inferredNotNullMethodAnnotation)) { + diffs.add(fieldKey + ": " + externalNotNullMethodAnnotation + " != " + inferredNotNullMethodAnnotation + "\n"); + } + } + public void _testExportInferredAnnotations() { PsiPackage rootPackage = JavaPsiFacade.getInstance(getProject()).findPackage(""); assertNotNull(rootPackage); @@ -201,6 +215,7 @@ public class BytecodeAnalysisIntegrationTest extends LightJavaCodeInsightFixture private void processClass(PsiClass aClass, Map> annotations) { for (PsiMethod method : aClass.getMethods()) annotateMethod(method, annotations); + for (PsiField field : aClass.getFields()) annotateField(field, annotations); for (PsiClass innerClass : aClass.getInnerClasses()) processClass(innerClass, annotations); } @@ -237,6 +252,13 @@ public class BytecodeAnalysisIntegrationTest extends LightJavaCodeInsightFixture } } + private void annotateField(PsiField field, Map> annotations) { + PsiAnnotation inferredNotNullMethodAnnotation = findInferredAnnotation(field, AnnotationUtil.NOT_NULL); + if (inferredNotNullMethodAnnotation != null) { + annotate(annotations, field, AnnotationUtil.NOT_NULL, PsiNameValuePair.EMPTY_ARRAY); + } + } + private void annotate(Map> annotations, PsiModifierListOwner owner, String annotationFQN, @@ -249,7 +271,8 @@ public class BytecodeAnalysisIntegrationTest extends LightJavaCodeInsightFixture if (annotations.isEmpty()) return; String xmlContent = EntryStream.of(annotations) .mapValues(map -> EntryStream.of(map).mapKeyValue(ExternalAnnotationsManagerImpl::createAnnotationTag).joining()) - .mapKeyValue((externalName, content) -> "\n" + content.trim() + "\n\n") + .mapKeyValue((externalName, content) -> "\n" + content.trim() + "\n\n") .joining("", "\n", ""); WriteCommandAction.runWriteCommandAction(getProject(), () -> { XmlFile xml = ExternalAnnotationsManagerImpl.createAnnotationsXml(root, packageName, getPsiManager()); diff --git a/java/java-tests/testSrc/com/intellij/java/codeInspection/bytecodeAnalysis/BytecodeAnalysisTest.java b/java/java-tests/testSrc/com/intellij/java/codeInspection/bytecodeAnalysis/BytecodeAnalysisTest.java index 0612ba07e55d..11777d6cc06b 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInspection/bytecodeAnalysis/BytecodeAnalysisTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInspection/bytecodeAnalysis/BytecodeAnalysisTest.java @@ -65,6 +65,7 @@ public class BytecodeAnalysisTest extends LightJavaCodeInsightFixtureTestCase { checkAnnotations("data.TestNonStable"); checkAnnotations("data.TestConflict"); checkAnnotations("data.TestEnum"); + checkAnnotations("data.TestField"); } public void testJava9Inference() { @@ -149,10 +150,15 @@ public class BytecodeAnalysisTest extends LightJavaCodeInsightFixtureTestCase { String inferredText = contractText(actualContract); assertEquals(displayName(method) + ":" + expectedText + " <> " + inferredText, expectedText, inferredText); } + for (PsiField field : psiClass.getFields()) { + boolean expectNotNull = AnnotationUtil.isAnnotated(field, EXPECT_NOT_NULL, 0); + PsiAnnotation actualAnnotation = service.findInferredAnnotation(field, AnnotationUtil.NOT_NULL); + assertNullity(displayName(field), expectNotNull, actualAnnotation); + } } - private static String displayName(PsiMethod method) { - return method.getContainingClass().getQualifiedName() + "." + method.getName(); + private static String displayName(PsiMember member) { + return member.getContainingClass().getQualifiedName() + "." + member.getName(); } private static String contractText(PsiAnnotation contract) {