[java-analysis] IDEA-361307 Provide index to map getter to the field in compiled code

GitOrigin-RevId: 8cf9a9628e922aae75c1366cde4150cf820d023f
This commit is contained in:
Tagir Valeev
2024-10-25 19:14:14 +00:00
committed by intellij-monorepo-bot
parent 8133c2d2cc
commit e8a61b4845
10 changed files with 149 additions and 10 deletions
@@ -186,6 +186,12 @@ public final class BytecodeAnalysisIndex extends ScalarIndexExtension<HMember> {
}
writeDataValue(out, effects.returnValue);
}
else if (rhs instanceof FieldAccess fieldAccess) {
out.writeUTF(fieldAccess.name());
}
else {
throw new UnsupportedOperationException("Unsupported result: " + rhs + " in " + eqs);
}
}
}
@@ -205,6 +211,9 @@ public final class BytecodeAnalysisIndex extends ScalarIndexExtension<HMember> {
DataValue returnValue = readDataValue(in);
results.add(new DirectionResultPair(directionKey, new Effects(returnValue, Set.copyOf(effects))));
}
else if (direction == Direction.Access) {
results.add(new DirectionResultPair(directionKey, new FieldAccess(in.readUTF())));
}
else {
boolean isFinal = in.readBoolean(); // flag
if (isFinal) {
@@ -22,7 +22,7 @@ import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.org.objectweb.asm.*;
import org.jetbrains.org.objectweb.asm.tree.MethodNode;
import org.jetbrains.org.objectweb.asm.tree.*;
import org.jetbrains.org.objectweb.asm.tree.analysis.AnalyzerException;
import java.io.DataOutputStream;
@@ -53,7 +53,7 @@ public class ClassDataIndexer implements VirtualFileGist.GistCalculator<Map<HMem
static final BinaryOperator<Equations> MERGER =
(eq1, eq2) -> eq1.equals(eq2) ? eq1 : new Equations(Collections.emptyList(), false);
private static final int VERSION = 16; // change when inference algorithm changes
private static final int VERSION = 17; // 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<Map<HMember, Equations>> ourGist = GistManager.getInstance().newVirtualFileGist(
@@ -199,7 +199,7 @@ public class ClassDataIndexer implements VirtualFileGist.GistCalculator<Map<HMem
return new Equations(compressedMethodEquations, methodKey.stable);
}
public static Map<EKey, Equations> processClass(final ClassReader classReader, final String presentableUrl) {
static Map<EKey, Equations> processClass(final ClassReader classReader, final String presentableUrl) {
// It is OK to share pending states, actions and results for analyses.
// Analyses are designed in such a way that they first write to states/actions/results and then read only those portion
@@ -641,6 +641,9 @@ public class ClassDataIndexer implements VirtualFileGist.GistCalculator<Map<HMem
boolean stable,
List<? super Equation> result) throws AnalyzerException {
Set<Member> fieldsToTrack = method.methodName.equals("<clinit>") ? myStaticFinalFields : Collections.emptySet();
if (argumentTypes.length == 0 && !Type.VOID_TYPE.equals(returnType)) {
ContainerUtil.addIfNotNull(result, getterEquation(method, graph, stable));
}
CombinedAnalysis analyzer = new CombinedAnalysis(method, graph, fieldsToTrack);
analyzer.analyze();
ContainerUtil.addIfNotNull(result, analyzer.outContractEquation(stable));
@@ -667,6 +670,40 @@ public class ClassDataIndexer implements VirtualFileGist.GistCalculator<Map<HMem
}
}
private @Nullable Equation getterEquation(@NotNull Member method, @NotNull ControlFlowGraph controlFlow, boolean stable) {
MethodNode node = controlFlow.methodNode;
boolean isStatic = (node.access & Opcodes.ACC_STATIC) != 0;
InsnList instructions = node.instructions;
int size = instructions.size();
int shift = isStatic ? 0 : 1;
if (size != 2 + shift) return null;
if (!isReturn(instructions.get(1 + shift))) return null;
// isStatic -> GETSTATIC + xRETURN
// !isStatic -> ALOAD_0 + GETFIELD + xRETURN
if (!isStatic) {
if (!(instructions.get(0) instanceof VarInsnNode varAccess) ||
varAccess.getOpcode() != Opcodes.ALOAD ||
varAccess.var != 0) {
return null;
}
}
if (!(instructions.get(shift) instanceof FieldInsnNode fieldAccess) ||
fieldAccess.getOpcode() != (isStatic ? Opcodes.GETSTATIC : Opcodes.GETFIELD) ||
!fieldAccess.owner.equals(className)) {
return null;
}
String name = fieldAccess.name;
if (!isReturn(instructions.get(1 + shift))) return null;
return new Equation(new EKey(method, Access, stable), new FieldAccess(name));
}
private static boolean isReturn(AbstractInsnNode insn) {
int opcode = insn.getOpcode();
return opcode == Opcodes.ARETURN || opcode == Opcodes.DRETURN ||
opcode == Opcodes.FRETURN || opcode == Opcodes.IRETURN ||
opcode == Opcodes.LRETURN;
}
private void storeStaticFieldEquations(CombinedAnalysis analyzer) {
for (Equation equation : analyzer.staticFieldEquations()) {
myEquations.put(equation.key,
@@ -129,7 +129,7 @@ class Equations {
}
}
class DirectionResultPair {
final class DirectionResultPair {
final int directionKey;
@NotNull
final Result result;
@@ -159,7 +159,7 @@ class DirectionResultPair {
}
}
interface Result {
sealed interface Result permits Effects, FieldAccess, Pending, Value {
/**
* @return a stream of keys which should be solved to make this result final
*/
@@ -171,6 +171,14 @@ interface Result {
}
}
/**
* A result for the {@link Direction#Access} direction:
* for setter/constructor parameter: unconditional field set;
* for method: unconditional field return
* @param name name of the field
*/
record FieldAccess(String name) implements Result {}
final class Pending implements Result {
final Component @NotNull [] delta; // sum
@@ -10,10 +10,11 @@ public abstract class Direction {
public static final Direction Out = explicitDirection("Out");
public static final Direction NullableOut = explicitDirection("NullableOut");
public static final Direction Pure = explicitDirection("Pure");
public static final Direction Access = explicitDirection("Access");
public static final Direction Throw = explicitDirection("Throw");
public static final Direction Volatile = explicitDirection("Volatile");
private static final List<Direction> ourConcreteDirections = Arrays.asList(Out, NullableOut, Pure, Throw, Volatile);
private static final List<Direction> ourConcreteDirections = Arrays.asList(Out, NullableOut, Pure, Access, Throw, Volatile);
private static final int CONCRETE_DIRECTIONS_OFFSET = ourConcreteDirections.size();
private static final int IN_OUT_OFFSET = 2; // nullity mask is 0/1
private static final int IN_THROW_OFFSET = 2 + Value.values().length;
@@ -69,6 +69,23 @@ public class ProjectBytecodeAnalysis {
nullableMethodTransitivity = Registry.is(NULLABLE_METHOD_TRANSITIVITY);
}
/**
* @param getter getter method
* @return field that this method reads and returns; null if the method is not identified as a getter
*/
public @Nullable PsiField findFieldForGetter(@NotNull PsiMethod getter) {
EKey eKey = getKey(getter);
if (eKey == null) return null;
EKey accessKey = myEquationProvider.adaptKey(eKey.withDirection(Access));
for (Equations equation : myEquationProvider.getEquations(accessKey.member)) {
if (equation.find(Access).orElse(null) instanceof FieldAccess access) {
PsiClass containingClass = getter.getContainingClass();
return containingClass != null ? containingClass.findFieldByName(access.name(), false) : null;
}
}
return null;
}
@Nullable
public PsiAnnotation findInferredAnnotation(@NotNull PsiModifierListOwner listOwner, @NotNull String annotationFQN) {
if (!(listOwner instanceof PsiCompiledElement)) {
@@ -1,6 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.util;
import com.intellij.codeInspection.bytecodeAnalysis.ProjectBytecodeAnalysis;
import com.intellij.lang.java.beans.PropertyKind;
import com.intellij.lang.jvm.JvmModifier;
import com.intellij.psi.*;
@@ -29,14 +30,26 @@ public final class PropertyUtil extends PropertyUtilBase {
@Nullable
public static PsiField getFieldOfGetter(PsiMethod method, Supplier<? extends PsiExpression> returnExprSupplier, boolean useIndex) {
PsiField field = useIndex && method instanceof PsiMethodImpl && method.isPhysical()
? JavaSimplePropertyGistKt.getFieldOfGetter(method)
: getSimplyReturnedField(returnExprSupplier.get());
PsiField field = getFieldImpl(method, returnExprSupplier, useIndex);
if (field == null || !checkFieldLocation(method, field)) return null;
final PsiType returnType = method.getReturnType();
return returnType != null && field.getType().equals(returnType) ? field : null;
}
private static @Nullable PsiField getFieldImpl(@NotNull PsiMethod method,
@NotNull Supplier<? extends PsiExpression> returnExprSupplier,
boolean useIndex) {
if (useIndex) {
if (PsiUtil.preferCompiledElement(method) instanceof PsiMethod compiledMethod) {
return ProjectBytecodeAnalysis.getInstance(method.getProject()).findFieldForGetter(compiledMethod);
}
if (method instanceof PsiMethodImpl && method.isPhysical()) {
return JavaSimplePropertyGistKt.getFieldOfGetter(method);
}
}
return getSimplyReturnedField(returnExprSupplier.get());
}
public static boolean isSimpleGetter(@Nullable PsiMethod method) {
//noinspection TestOnlyProblems
return isSimpleGetter(method, true);
@@ -0,0 +1,18 @@
import java.util.*;
class Test {
void testVersion() {
Runtime runtime = Runtime.getRuntime();
Runtime.Version version = runtime.version();
if (version.build().isPresent()) {
unknown();
if (<warning descr="Condition 'version.build().isPresent()' is always 'true'">version.build().isPresent()</warning>) {
}
if (<warning descr="Condition 'runtime == Runtime.getRuntime()' is always 'true'">runtime == Runtime.getRuntime()</warning>) {
}
}
}
native void unknown();
}
@@ -140,4 +140,8 @@ public class DataFlowInspection21Test extends DataFlowInspectionTestCase {
addJetBrainsNotNullByDefault(myFixture);
doTest();
}
public void testClassFileGetter() {
doTest();
}
}
@@ -0,0 +1,32 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInspection.bytecodeAnalysis;
import com.intellij.codeInspection.bytecodeAnalysis.ProjectBytecodeAnalysis;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
public final class BytecodeAccessorTest extends LightJavaCodeInsightFixtureTestCase {
public void testAccessors() {
PsiClass psiClass =
JavaPsiFacade.getInstance(getProject()).findClass("java.util.OptionalInt", GlobalSearchScope.allScope(getProject()));
assertNotNull(psiClass);
assertInstanceOf(psiClass, PsiCompiledElement.class);
PsiMethod isPresentMethod = psiClass.findMethodsByName("isPresent", false)[0];
assertFalse(isPresentMethod.hasModifierProperty(PsiModifier.STATIC));
PsiMethod emptyMethod = psiClass.findMethodsByName("empty", false)[0];
assertTrue(emptyMethod.hasModifierProperty(PsiModifier.STATIC));
ProjectBytecodeAnalysis analysis = ProjectBytecodeAnalysis.getInstance(getProject());
PsiField isPresentField = analysis.findFieldForGetter(isPresentMethod);
assertNotNull(isPresentField);
PsiField emptyField = analysis.findFieldForGetter(emptyMethod);
assertNotNull(emptyField);
}
@Override
protected @NotNull LightProjectDescriptor getProjectDescriptor() {
return JAVA_21;
}
}
@@ -169,7 +169,7 @@ public class BytecodeAnalysisTest extends LightJavaCodeInsightFixtureTestCase {
fail(message + ": @NotNull inferred, but not expected");
}
}
private void checkCompoundIds(String className) throws IOException {
GlobalSearchScope scope = GlobalSearchScope.moduleWithLibrariesScope(getModule());
PsiClass psiClass = JavaPsiFacade.getInstance(getProject()).findClass(PACKAGE_NAME + '.' + className, scope);