Skip to content

Commit 11b54a4

Browse files
Seppli11sonartech
authored andcommitted
SONARPY-3260 Let SelfType collapse when returned from a classmethod (#701)
GitOrigin-RevId: 0c5645271f765483ed1995e1484d6da7c9ffa5b5
1 parent 0e2a430 commit 11b54a4

File tree

17 files changed

+295
-53
lines changed

17 files changed

+295
-53
lines changed

python-commons/src/test/java/org/sonar/plugins/python/caching/CachingTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ void writeProjectLevelSymbolTableEntry() throws InvalidProtocolBufferException {
6969
Caching caching = new Caching(cacheContext, CACHE_VERSION);
7070
Set<Descriptor> initialDescriptors = Set.of(
7171
new ClassDescriptor.ClassDescriptorBuilder().withName("C").withFullyQualifiedName("mod.C").build(),
72-
new FunctionDescriptor("foo", "mod.foo", Collections.emptyList(), false, false, Collections.emptyList(), false, null, null),
72+
new FunctionDescriptor("foo", "mod.foo", Collections.emptyList(), false, false, false, Collections.emptyList(), false, null, null),
7373
new VariableDescriptor("x", "mod.x", null)
7474
);
7575
caching.writeProjectLevelSymbolTableEntry("mod", initialDescriptors);
@@ -92,7 +92,7 @@ void readProjectLevelSymbolTableEntry() {
9292
Caching caching = new Caching(cacheContext, CACHE_VERSION);
9393
Set<Descriptor> initialDescriptors = Set.of(
9494
new ClassDescriptor.ClassDescriptorBuilder().withName("C").withFullyQualifiedName("mod.C").build(),
95-
new FunctionDescriptor("foo", "mod.foo", Collections.emptyList(), false, false, Collections.emptyList(), false, null, null),
95+
new FunctionDescriptor("foo", "mod.foo", Collections.emptyList(), false, false, false, Collections.emptyList(), false, null, null),
9696
new VariableDescriptor("x", "mod.x", null)
9797
);
9898
String cacheKey = PROJECT_SYMBOL_TABLE_CACHE_KEY_PREFIX + "mod";

python-frontend/src/main/java/org/sonar/plugins/python/api/types/v2/FunctionType.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public final class FunctionType implements PythonType {
3838
private final boolean isAsynchronous;
3939
private final boolean hasDecorators;
4040
private final boolean isInstanceMethod;
41+
private final boolean isClassMethod;
4142
private final boolean hasVariadicParameter;
4243
private final PythonType owner;
4344
private final LocationInFile locationInFile;
@@ -56,6 +57,7 @@ public FunctionType(
5657
boolean isAsynchronous,
5758
boolean hasDecorators,
5859
boolean isInstanceMethod,
60+
boolean isClassMethod,
5961
boolean hasVariadicParameter,
6062
@Nullable PythonType owner,
6163
@Nullable LocationInFile locationInFile
@@ -70,6 +72,7 @@ public FunctionType(
7072
this.isAsynchronous = isAsynchronous;
7173
this.hasDecorators = hasDecorators;
7274
this.isInstanceMethod = isInstanceMethod;
75+
this.isClassMethod = isClassMethod;
7376
this.hasVariadicParameter = hasVariadicParameter;
7477
this.owner = owner;
7578
this.locationInFile = locationInFile;
@@ -123,6 +126,10 @@ public boolean isInstanceMethod() {
123126
return isInstanceMethod;
124127
}
125128

129+
public boolean isClassMethod() {
130+
return isClassMethod;
131+
}
132+
126133
public boolean hasVariadicParameter() {
127134
return hasVariadicParameter;
128135
}

python-frontend/src/main/java/org/sonar/python/index/DescriptorsToProtobuf.java

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ private static DescriptorsProtos.FunctionDescriptor toProtobuf(FunctionDescripto
129129
.addAllParameters(functionDescriptor.parameters().stream().map(DescriptorsToProtobuf::toProtobuf).toList())
130130
.setIsAsynchronous(functionDescriptor.isAsynchronous())
131131
.setIsInstanceMethod(functionDescriptor.isInstanceMethod())
132+
.setIsClassMethod(functionDescriptor.isClassMethod())
132133
.addAllDecorators(functionDescriptor.decorators())
133134
.setHasDecorators(functionDescriptor.hasDecorators());
134135
String annotatedReturnTypeName = functionDescriptor.annotatedReturnTypeName();
@@ -262,17 +263,19 @@ private static FunctionDescriptor fromProtobuf(DescriptorsProtos.FunctionDescrip
262263
TypeAnnotationDescriptor returnTypeAnnotationDescriptor = functionDescriptorProto.hasReturnTypeAnnotationDescriptor()
263264
? fromProtobuf(functionDescriptorProto.getReturnTypeAnnotationDescriptor())
264265
: null;
265-
return new FunctionDescriptor(
266-
functionDescriptorProto.getName(),
267-
fullyQualifiedName,
268-
parameters,
269-
functionDescriptorProto.getIsAsynchronous(),
270-
functionDescriptorProto.getIsInstanceMethod(),
271-
new ArrayList<>(functionDescriptorProto.getDecoratorsList()),
272-
functionDescriptorProto.getHasDecorators(),
273-
definitionLocation,
274-
annotatedReturnTypeName,
275-
returnTypeAnnotationDescriptor);
266+
return new FunctionDescriptor.FunctionDescriptorBuilder()
267+
.withName(functionDescriptorProto.getName())
268+
.withFullyQualifiedName(fullyQualifiedName)
269+
.withParameters(parameters)
270+
.withIsAsynchronous(functionDescriptorProto.getIsAsynchronous())
271+
.withIsInstanceMethod(functionDescriptorProto.getIsInstanceMethod())
272+
.withIsClassMethod(functionDescriptorProto.getIsClassMethod())
273+
.withDecorators(new ArrayList<>(functionDescriptorProto.getDecoratorsList()))
274+
.withHasDecorators(functionDescriptorProto.getHasDecorators())
275+
.withDefinitionLocation(definitionLocation)
276+
.withAnnotatedReturnTypeName(annotatedReturnTypeName)
277+
.withTypeAnnotationDescriptor(returnTypeAnnotationDescriptor)
278+
.build();
276279
}
277280

278281
private static FunctionDescriptor.Parameter fromProtobuf(DescriptorsProtos.ParameterDescriptor parameterDescriptorProto) {
@@ -329,4 +332,4 @@ private static TypeAnnotationDescriptor fromProtobuf(SymbolsProtos.Type typeProt
329332
fullyQualifiedName,
330333
typeProto.getIsSelf());
331334
}
332-
}
335+
}

python-frontend/src/main/java/org/sonar/python/index/FunctionDescriptor.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public class FunctionDescriptor implements Descriptor {
3030
private final List<Parameter> parameters;
3131
private final boolean isAsynchronous;
3232
private final boolean isInstanceMethod;
33+
private final boolean isClassMethod;
3334
private final List<String> decorators;
3435
private final boolean hasDecorators;
3536
@Nullable
@@ -41,19 +42,21 @@ public class FunctionDescriptor implements Descriptor {
4142

4243

4344
public FunctionDescriptor(String name, String fullyQualifiedName, List<Parameter> parameters, boolean isAsynchronous,
44-
boolean isInstanceMethod, List<String> decorators, boolean hasDecorators, @Nullable LocationInFile definitionLocation, @Nullable String annotatedReturnTypeName) {
45-
this(name, fullyQualifiedName, parameters, isAsynchronous, isInstanceMethod, decorators, hasDecorators, definitionLocation, annotatedReturnTypeName, null);
45+
boolean isInstanceMethod, boolean isClassMethod, List<String> decorators, boolean hasDecorators,
46+
@Nullable LocationInFile definitionLocation, @Nullable String annotatedReturnTypeName) {
47+
this(name, fullyQualifiedName, parameters, isAsynchronous, isInstanceMethod, isClassMethod, decorators, hasDecorators, definitionLocation, annotatedReturnTypeName, null);
4648
}
4749

4850
public FunctionDescriptor(String name, String fullyQualifiedName, List<Parameter> parameters, boolean isAsynchronous,
49-
boolean isInstanceMethod, List<String> decorators, boolean hasDecorators, @Nullable LocationInFile definitionLocation,
50-
@Nullable String annotatedReturnTypeName, @Nullable TypeAnnotationDescriptor typeAnnotationDescriptor) {
51+
boolean isInstanceMethod, boolean isClassMethod, List<String> decorators, boolean hasDecorators, @Nullable LocationInFile definitionLocation,
52+
@Nullable String annotatedReturnTypeName, @Nullable TypeAnnotationDescriptor typeAnnotationDescriptor) {
5153

5254
this.name = name;
5355
this.fullyQualifiedName = fullyQualifiedName;
5456
this.parameters = parameters;
5557
this.isAsynchronous = isAsynchronous;
5658
this.isInstanceMethod = isInstanceMethod;
59+
this.isClassMethod = isClassMethod;
5760
this.decorators = decorators;
5861
this.hasDecorators = hasDecorators;
5962
this.definitionLocation = definitionLocation;
@@ -89,6 +92,10 @@ public boolean isInstanceMethod() {
8992
return isInstanceMethod;
9093
}
9194

95+
public boolean isClassMethod() {
96+
return isClassMethod;
97+
}
98+
9299
public List<String> decorators() {
93100
return decorators;
94101
}
@@ -188,6 +195,7 @@ public static class FunctionDescriptorBuilder {
188195
private List<Parameter> parameters = new ArrayList<>();
189196
private boolean isAsynchronous = false;
190197
private boolean isInstanceMethod = false;
198+
private boolean isClassMethod = false;
191199
private List<String> decorators = new ArrayList<>();
192200
private boolean hasDecorators = false;
193201
private LocationInFile definitionLocation = null;
@@ -219,6 +227,11 @@ public FunctionDescriptorBuilder withIsInstanceMethod(boolean isInstanceMethod)
219227
return this;
220228
}
221229

230+
public FunctionDescriptorBuilder withIsClassMethod(boolean isClassMethod) {
231+
this.isClassMethod = isClassMethod;
232+
return this;
233+
}
234+
222235
public FunctionDescriptorBuilder withDecorators(List<String> decorators) {
223236
this.decorators = decorators;
224237
return this;
@@ -245,8 +258,8 @@ public FunctionDescriptorBuilder withTypeAnnotationDescriptor(@Nullable TypeAnno
245258
}
246259

247260
public FunctionDescriptor build() {
248-
return new FunctionDescriptor(name, fullyQualifiedName, parameters, isAsynchronous, isInstanceMethod, decorators,
249-
hasDecorators, definitionLocation, annotatedReturnTypeName, typeAnnotationDescriptor);
261+
return new FunctionDescriptor(name, fullyQualifiedName, parameters, isAsynchronous, isInstanceMethod, isClassMethod, decorators,
262+
hasDecorators, definitionLocation, annotatedReturnTypeName, typeAnnotationDescriptor);
250263
}
251264
}
252265
}

python-frontend/src/main/java/org/sonar/python/semantic/v2/FunctionTypeBuilder.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@
3131
import org.sonar.plugins.python.api.tree.ParameterList;
3232
import org.sonar.plugins.python.api.tree.Token;
3333
import org.sonar.plugins.python.api.tree.Tree;
34-
import org.sonar.python.semantic.v2.typetable.TypeTable;
35-
import org.sonar.python.tree.TreeUtils;
3634
import org.sonar.plugins.python.api.types.v2.FunctionType;
3735
import org.sonar.plugins.python.api.types.v2.ParameterV2;
3836
import org.sonar.plugins.python.api.types.v2.PythonType;
39-
import org.sonar.python.types.v2.SimpleTypeWrapper;
4037
import org.sonar.plugins.python.api.types.v2.TypeOrigin;
4138
import org.sonar.plugins.python.api.types.v2.TypeWrapper;
39+
import org.sonar.python.semantic.v2.typetable.TypeTable;
40+
import org.sonar.python.tree.TreeUtils;
41+
import org.sonar.python.types.v2.SimpleTypeWrapper;
4242

4343
import static org.sonar.python.tree.TreeUtils.locationInFile;
4444

@@ -53,6 +53,7 @@ public class FunctionTypeBuilder implements TypeBuilder<FunctionType> {
5353
private boolean isAsynchronous;
5454
private boolean hasDecorators;
5555
private boolean isInstanceMethod;
56+
private boolean isClassMethod;
5657
private PythonType owner;
5758
private TypeWrapper returnType = TypeWrapper.UNKNOWN_TYPE_WRAPPER;
5859
private TypeOrigin typeOrigin = TypeOrigin.STUB;
@@ -75,6 +76,7 @@ public FunctionTypeBuilder fromFunctionDef(FunctionDef functionDef, String fully
7576
.map(TypeWrapper::of)
7677
.toList();
7778
isInstanceMethod = isInstanceMethod(functionDef);
79+
isClassMethod = isClassMethod(functionDef);
7880
ParameterList parameterList = functionDef.parameters();
7981
if (parameterList != null) {
8082
createParameterNames(parameterList.all(), fileId, projectLevelTypeTable);
@@ -134,6 +136,11 @@ public FunctionTypeBuilder withInstanceMethod(boolean instanceMethod) {
134136
return this;
135137
}
136138

139+
public FunctionTypeBuilder withClassMethod(boolean classMethod) {
140+
isClassMethod = classMethod;
141+
return this;
142+
}
143+
137144
public FunctionTypeBuilder withReturnType(PythonType returnType) {
138145
withReturnType(TypeWrapper.of(returnType));
139146
return this;
@@ -155,10 +162,11 @@ public FunctionTypeBuilder withDefinitionLocation(@Nullable LocationInFile defin
155162
return this;
156163
}
157164

165+
@Override
158166
public FunctionType build() {
159167
return new FunctionType(
160168
name, fullyQualifiedName, attributes, parameters, decorators, returnType, typeOrigin,
161-
isAsynchronous, hasDecorators, isInstanceMethod, hasVariadicParameter, owner, definitionLocation
169+
isAsynchronous, hasDecorators, isInstanceMethod, isClassMethod, hasVariadicParameter, owner, definitionLocation
162170
);
163171
}
164172

@@ -169,6 +177,13 @@ private static boolean isInstanceMethod(FunctionDef functionDef) {
169177
.noneMatch(decorator -> decorator.equals(STATIC_METHOD_DECORATOR) || decorator.equals(CLASS_METHOD_DECORATOR));
170178
}
171179

180+
private static boolean isClassMethod(FunctionDef functionDef) {
181+
return functionDef.isMethodDefinition() && functionDef.decorators().stream()
182+
.map(decorator -> TreeUtils.decoratorNameFromExpression(decorator.expression()))
183+
.filter(Objects::nonNull)
184+
.anyMatch(CLASS_METHOD_DECORATOR::equals);
185+
}
186+
172187
public FunctionTypeBuilder withOwner(PythonType owner) {
173188
this.owner = owner;
174189
return this;

python-frontend/src/main/java/org/sonar/python/semantic/v2/converter/FunctionDescriptorToPythonTypeConverter.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919
import java.util.List;
2020
import java.util.Objects;
2121
import java.util.Optional;
22+
import org.sonar.plugins.python.api.types.v2.ParameterV2;
23+
import org.sonar.plugins.python.api.types.v2.PythonType;
24+
import org.sonar.plugins.python.api.types.v2.TypeWrapper;
2225
import org.sonar.python.index.Descriptor;
2326
import org.sonar.python.index.FunctionDescriptor;
2427
import org.sonar.python.semantic.v2.FunctionTypeBuilder;
25-
import org.sonar.plugins.python.api.types.v2.ParameterV2;
26-
import org.sonar.plugins.python.api.types.v2.PythonType;
2728
import org.sonar.python.types.v2.TypeUtils;
28-
import org.sonar.plugins.python.api.types.v2.TypeWrapper;
2929

3030
public class FunctionDescriptorToPythonTypeConverter implements DescriptorToPythonTypeConverter {
3131

@@ -70,6 +70,7 @@ public PythonType convert(ConversionContext ctx, FunctionDescriptor from) {
7070
.withAsynchronous(from.isAsynchronous())
7171
.withHasDecorators(from.hasDecorators())
7272
.withInstanceMethod(from.isInstanceMethod())
73+
.withClassMethod(from.isClassMethod())
7374
.withHasVariadicParameter(hasVariadicParameter)
7475
.withDefinitionLocation(from.definitionLocation());
7576

python-frontend/src/main/java/org/sonar/python/semantic/v2/converter/PythonTypeToDescriptorConverter.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ private static Descriptor convert(FunctionType type) {
121121
parameters,
122122
type.isAsynchronous(),
123123
type.isInstanceMethod(),
124+
type.isClassMethod(),
124125
decorators,
125126
type.hasDecorators(),
126127
type.definitionLocation().orElse(null),

python-frontend/src/main/java/org/sonar/python/semantic/v2/types/CallReturnTypeCalculator.java

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import java.util.Optional;
2121
import java.util.Set;
2222
import org.sonar.plugins.python.api.tree.CallExpression;
23+
import org.sonar.plugins.python.api.tree.Expression;
24+
import org.sonar.plugins.python.api.tree.Name;
2325
import org.sonar.plugins.python.api.tree.QualifiedExpression;
2426
import org.sonar.plugins.python.api.types.v2.ClassType;
2527
import org.sonar.plugins.python.api.types.v2.FunctionType;
@@ -81,38 +83,50 @@ private static PythonType collapseSelfTypeIfNeeded(CallExpression callExpr, Pyth
8183
return returnType;
8284
}
8385

84-
// Methods not called as instance methods don't have a receiver from which the return type can be inferred
85-
PythonType receiverType = getReceiverType(callExpr);
86-
if (!isInstanceMethodCall(callExpr) || receiverType == PythonType.UNKNOWN) {
87-
return PythonType.UNKNOWN;
86+
if (isInstanceOrClassMethodCall(callExpr, typePredicateContext)) {
87+
PythonType receiverType = getReceiverType(callExpr);
88+
if (receiverType == PythonType.UNKNOWN) {
89+
return PythonType.UNKNOWN;
90+
}
91+
return collapseSelfType(returnType, receiverType);
8892
}
8993

90-
return collapseSelfType(returnType, receiverType);
94+
return PythonType.UNKNOWN;
9195
}
9296

9397
private static boolean containsSelfType(PythonType type, TypePredicateContext typePredicateContext) {
94-
return TypeInferenceMatcher.of(
95-
TypeInferenceMatchers.any(
96-
TypeInferenceMatchers.isSelf(),
97-
TypeInferenceMatchers.isObjectSatisfying(TypeInferenceMatchers.isSelf())))
98+
return TypeInferenceMatcher.of(TypeInferenceMatchers.isObjectSatisfying(TypeInferenceMatchers.isSelf()))
9899
.evaluate(type, typePredicateContext).isTrue();
99100
}
100101

101-
private static boolean isInstanceMethodCall(CallExpression callExpr) {
102+
private static boolean isInstanceOrClassMethodCall(CallExpression callExpr, TypePredicateContext typePredicateContext) {
102103
PythonType calleeType = callExpr.callee().typeV2();
103104
if (calleeType instanceof FunctionType functionType) {
104-
return functionType.isInstanceMethod();
105+
return functionType.isInstanceMethod() || functionType.isClassMethod();
105106
}
106-
return false;
107+
return hasCallableMember(calleeType, typePredicateContext);
108+
}
109+
110+
private static boolean hasCallableMember(PythonType calleeType, TypePredicateContext typePredicateContext) {
111+
return TypeInferenceMatcher.of(TypeInferenceMatchers.isObjectSatisfying(TypeInferenceMatchers.hasMember("__call__")))
112+
.evaluate(calleeType, typePredicateContext).isTrue();
107113
}
108114

109115
private static PythonType getReceiverType(CallExpression callExpr) {
110-
if (callExpr.callee() instanceof QualifiedExpression qualifiedExpr) {
116+
Expression callee = callExpr.callee();
117+
if (callee instanceof QualifiedExpression qualifiedExpr) {
111118
PythonType qualifierType = qualifiedExpr.qualifier().typeV2();
119+
if (qualifierType instanceof ClassType classType) {
120+
return classType;
121+
}
112122
if (qualifierType instanceof ObjectType objectType) {
113123
return objectType.type();
114124
}
125+
} else if (callee instanceof Name name && name.typeV2() instanceof ObjectType objectType) {
126+
// This covers a class with a __call__ member than returns Self
127+
return objectType.unwrappedType();
115128
}
129+
116130
return PythonType.UNKNOWN;
117131
}
118132

python-frontend/src/main/java/org/sonar/python/semantic/v2/types/TypeInferenceMatchers.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.sonar.python.api.types.v2.matchers.TypeMatchers;
2222
import org.sonar.python.types.v2.matchers.AnyTypePredicate;
2323
import org.sonar.python.types.v2.matchers.HasFQNPredicate;
24+
import org.sonar.python.types.v2.matchers.HasMemberPredicate;
2425
import org.sonar.python.types.v2.matchers.IsObjectSatisfyingPredicate;
2526
import org.sonar.python.types.v2.matchers.IsSelfTypePredicate;
2627
import org.sonar.python.types.v2.matchers.IsTypePredicate;
@@ -54,6 +55,10 @@ public static TypePredicate isObjectOfType(String fqn) {
5455
return isObjectSatisfying(isType(fqn));
5556
}
5657

58+
public static TypePredicate hasMember(String memberName) {
59+
return new HasMemberPredicate(memberName);
60+
}
61+
5762
public static TypePredicate isSelf() {
5863
return new IsSelfTypePredicate();
5964
}

python-frontend/src/main/java/org/sonar/python/semantic/v2/typeshed/FunctionSymbolToDescriptorConverter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ public FunctionDescriptor convert(SymbolsProtos.FunctionSymbol functionSymbol, b
5454
.map(parameterConverter::convert)
5555
.toList();
5656
var isInstanceMethod = isParentIsAClass && !functionSymbol.getIsStatic() && !functionSymbol.getIsClassMethod();
57+
var isClassMethod = isParentIsAClass && !functionSymbol.getIsStatic() && functionSymbol.getIsClassMethod();
5758
return new FunctionDescriptor.FunctionDescriptorBuilder()
5859
.withName(functionSymbol.getName())
5960
.withFullyQualifiedName(fullyQualifiedName)
6061
.withIsAsynchronous(functionSymbol.getIsAsynchronous())
6162
.withIsInstanceMethod(isInstanceMethod)
63+
.withIsClassMethod(isClassMethod)
6264
.withHasDecorators(functionSymbol.getHasDecorators())
6365
.withAnnotatedReturnTypeName(returnType)
6466
.withTypeAnnotationDescriptor(typeAnnotationDescriptor)

0 commit comments

Comments
 (0)