Skip to content

Commit 08ebeb9

Browse files
Seppli11sonartech
authored andcommitted
SONARPY-3635 Resolve type @Property methods correctly with Ast-based type inference in function with no parameters (#763)
GitOrigin-RevId: 6993eb12048f5f35e18e651064710f39e26c88d3
1 parent 586adf4 commit 08ebeb9

13 files changed

Lines changed: 274 additions & 68 deletions

File tree

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import org.sonar.python.types.v2.matchers.HasMemberSatisfyingPredicate;
2929
import org.sonar.python.types.v2.matchers.IsFunctionOwnerSatisfyingPredicate;
3030
import org.sonar.python.types.v2.matchers.IsObjectSatisfyingPredicate;
31-
import org.sonar.python.types.v2.matchers.IsObjectSubtypeOfPredicate;
31+
import org.sonar.python.types.v2.matchers.IsSubtypeOfPredicate;
3232
import org.sonar.python.types.v2.matchers.IsTypeOrSuperTypeSatisfyingPredicate;
3333
import org.sonar.python.types.v2.matchers.IsTypePredicate;
3434
import org.sonar.python.types.v2.matchers.TypePredicate;
@@ -95,8 +95,12 @@ public static TypeMatcher isObjectOfType(String fqn) {
9595
return isObjectSatisfying(isType(fqn));
9696
}
9797

98+
public static TypeMatcher isSubtypeOf(String fqn) {
99+
return new TypeMatcherImpl(new IsSubtypeOfPredicate(fqn));
100+
}
101+
98102
public static TypeMatcher isObjectOfSubType(String fqn) {
99-
return new TypeMatcherImpl(new IsObjectSubtypeOfPredicate(fqn));
103+
return isObjectSatisfying(isSubtypeOf(fqn));
100104
}
101105

102106
public static TypeMatcher isOrExtendsType(String fqn) {
@@ -131,7 +135,7 @@ public static TypeMatcher hasMemberSatisfying(String memberName, TypeMatcher mat
131135

132136
@VisibleForTesting
133137
static TypePredicate getTypePredicate(TypeMatcher matcher) {
134-
if(matcher instanceof TypeMatcherImpl typeMatcherImpl) {
138+
if (matcher instanceof TypeMatcherImpl typeMatcherImpl) {
135139
return typeMatcherImpl.predicate();
136140
}
137141
throw new IllegalArgumentException("Unsupported type matcher: " + matcher.getClass().getName());

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@
8686
import org.sonar.plugins.python.api.types.v2.UnknownType;
8787
import org.sonar.python.semantic.v2.ClassTypeBuilder;
8888
import org.sonar.python.semantic.v2.FunctionTypeBuilder;
89+
import org.sonar.python.semantic.v2.types.typecalculator.AwaitedTypeCalculator;
90+
import org.sonar.python.semantic.v2.types.typecalculator.CallReturnTypeCalculator;
91+
import org.sonar.python.semantic.v2.types.typecalculator.QualifiedExpressionCalculator;
8992
import org.sonar.python.semantic.v2.typetable.TypeTable;
9093
import org.sonar.python.tree.AwaitExpressionImpl;
9194
import org.sonar.python.tree.BinaryExpressionImpl;
@@ -821,12 +824,10 @@ private boolean isTypingSelf(PythonType type) {
821824

822825
@Override
823826
public void visitQualifiedExpression(QualifiedExpression qualifiedExpression) {
824-
scan(qualifiedExpression.qualifier());
827+
super.visitQualifiedExpression(qualifiedExpression);
828+
PythonType type = new QualifiedExpressionCalculator(typePredicateContext).calculate(qualifiedExpression);
825829
if (qualifiedExpression.name() instanceof NameImpl name) {
826-
var nameType = qualifiedExpression.qualifier().typeV2()
827-
.resolveMember(qualifiedExpression.name().name())
828-
.orElse(PythonType.UNKNOWN);
829-
name.typeV2(nameType);
830+
name.typeV2(type);
830831
}
831832
}
832833

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

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,26 @@
1717
package org.sonar.python.semantic.v2.types;
1818

1919
import java.util.EnumSet;
20-
import java.util.Optional;
2120
import java.util.Set;
2221
import org.sonar.plugins.python.api.TriBool;
2322
import org.sonar.plugins.python.api.tree.AwaitExpression;
2423
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
2524
import org.sonar.plugins.python.api.tree.BinaryExpression;
2625
import org.sonar.plugins.python.api.tree.CallExpression;
2726
import org.sonar.plugins.python.api.tree.ConditionalExpression;
28-
import org.sonar.plugins.python.api.tree.Expression;
2927
import org.sonar.plugins.python.api.tree.QualifiedExpression;
3028
import org.sonar.plugins.python.api.tree.SliceExpression;
3129
import org.sonar.plugins.python.api.tree.Tree;
3230
import org.sonar.plugins.python.api.tree.UnaryExpression;
3331
import org.sonar.plugins.python.api.types.BuiltinTypes;
3432
import org.sonar.plugins.python.api.types.v2.ClassType;
35-
import org.sonar.plugins.python.api.types.v2.FunctionType;
3633
import org.sonar.plugins.python.api.types.v2.ObjectType;
3734
import org.sonar.plugins.python.api.types.v2.PythonType;
3835
import org.sonar.plugins.python.api.types.v2.TypeSource;
3936
import org.sonar.plugins.python.api.types.v2.UnionType;
37+
import org.sonar.python.semantic.v2.types.typecalculator.AwaitedTypeCalculator;
38+
import org.sonar.python.semantic.v2.types.typecalculator.CallReturnTypeCalculator;
39+
import org.sonar.python.semantic.v2.types.typecalculator.QualifiedExpressionCalculator;
4040
import org.sonar.python.semantic.v2.typetable.TypeTable;
4141
import org.sonar.python.tree.AwaitExpressionImpl;
4242
import org.sonar.python.tree.BinaryExpressionImpl;
@@ -96,22 +96,8 @@ public TrivialTypePropagationVisitor(TypeTable typeTable) {
9696
public void visitQualifiedExpression(QualifiedExpression qualifiedExpression) {
9797
scan(qualifiedExpression.qualifier());
9898
if (qualifiedExpression.name() instanceof NameImpl name) {
99-
Optional<PythonType> pythonType = Optional.of(qualifiedExpression.qualifier())
100-
.map(Expression::typeV2)
101-
.flatMap(t -> t.resolveMember(name.name()));
102-
if (pythonType.isPresent()) {
103-
var type = pythonType.get();
104-
if (type instanceof FunctionType functionType) {
105-
// If a member access is a method with a "property" annotation, we consider the resulting type to be the return type of the method
106-
boolean isProperty = functionType.decorators().stream().anyMatch(t -> isPropertyTypeCheck.check(t.type()) == TriBool.TRUE);
107-
if (isProperty) {
108-
type = functionType.returnType();
109-
}
110-
}
111-
name.typeV2(type);
112-
} else {
113-
name.typeV2(PythonType.UNKNOWN);
114-
}
99+
PythonType type = new QualifiedExpressionCalculator(typePredicateContext).calculate(qualifiedExpression);
100+
name.typeV2(type);
115101
}
116102
}
117103

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import org.sonar.python.types.v2.matchers.TypePredicateContext;
2323
import org.sonar.python.types.v2.matchers.TypePredicateUtils;
2424

25-
class TypeInferenceMatcher {
25+
public class TypeInferenceMatcher {
2626
private final TypePredicate predicate;
2727

2828
private TypeInferenceMatcher(TypePredicate predicate) {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.sonar.python.types.v2.matchers.HasMemberPredicate;
2525
import org.sonar.python.types.v2.matchers.IsObjectSatisfyingPredicate;
2626
import org.sonar.python.types.v2.matchers.IsSelfTypePredicate;
27+
import org.sonar.python.types.v2.matchers.IsSubtypeOfPredicate;
2728
import org.sonar.python.types.v2.matchers.IsTypePredicate;
2829
import org.sonar.python.types.v2.matchers.TypePredicate;
2930

@@ -35,7 +36,7 @@
3536
*
3637
* @see TypeInferenceMatcher
3738
*/
38-
class TypeInferenceMatchers {
39+
public class TypeInferenceMatchers {
3940
private TypeInferenceMatchers() {
4041
}
4142

@@ -55,6 +56,10 @@ public static TypePredicate isObjectOfType(String fqn) {
5556
return isObjectSatisfying(isType(fqn));
5657
}
5758

59+
public static TypePredicate isSubtypeOf(String fqn) {
60+
return new IsSubtypeOfPredicate(fqn);
61+
}
62+
5863
public static TypePredicate hasMember(String memberName) {
5964
return new HasMemberPredicate(memberName);
6065
}

python-frontend/src/main/java/org/sonar/python/semantic/v2/types/AwaitedTypeCalculator.java renamed to python-frontend/src/main/java/org/sonar/python/semantic/v2/types/typecalculator/AwaitedTypeCalculator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
* You should have received a copy of the Sonar Source-Available License
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
17-
package org.sonar.python.semantic.v2.types;
17+
package org.sonar.python.semantic.v2.types.typecalculator;
1818

1919
import java.util.List;
2020
import org.sonar.plugins.python.api.TriBool;
2121
import org.sonar.plugins.python.api.types.v2.ObjectType;
2222
import org.sonar.plugins.python.api.types.v2.PythonType;
23+
import org.sonar.python.semantic.v2.types.TypeInferenceMatcher;
24+
import org.sonar.python.semantic.v2.types.TypeInferenceMatchers;
2325
import org.sonar.python.semantic.v2.typetable.TypeTable;
2426
import org.sonar.python.types.v2.matchers.TypePredicateContext;
2527

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* You should have received a copy of the Sonar Source-Available License
1515
* along with this program; if not, see https://sonarsource.com/license/ssal/
1616
*/
17-
package org.sonar.python.semantic.v2.types;
17+
package org.sonar.python.semantic.v2.types.typecalculator;
1818

1919
import java.util.HashSet;
2020
import java.util.Optional;
@@ -32,6 +32,8 @@
3232
import org.sonar.plugins.python.api.types.v2.TypeSource;
3333
import org.sonar.plugins.python.api.types.v2.UnionType;
3434
import org.sonar.plugins.python.api.types.v2.UnknownType;
35+
import org.sonar.python.semantic.v2.types.TypeInferenceMatcher;
36+
import org.sonar.python.semantic.v2.types.TypeInferenceMatchers;
3537
import org.sonar.python.types.v2.matchers.TypePredicateContext;
3638

3739
public final class CallReturnTypeCalculator {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.semantic.v2.types.typecalculator;
18+
19+
import java.util.Optional;
20+
import org.sonar.plugins.python.api.tree.Expression;
21+
import org.sonar.plugins.python.api.tree.Name;
22+
import org.sonar.plugins.python.api.tree.QualifiedExpression;
23+
import org.sonar.plugins.python.api.types.v2.FunctionType;
24+
import org.sonar.plugins.python.api.types.v2.PythonType;
25+
import org.sonar.python.semantic.v2.types.TypeInferenceMatcher;
26+
import org.sonar.python.semantic.v2.types.TypeInferenceMatchers;
27+
import org.sonar.python.types.v2.matchers.TypePredicateContext;
28+
29+
public class QualifiedExpressionCalculator {
30+
private static final TypeInferenceMatcher IS_PROPERTY_TYPE = TypeInferenceMatcher.of(
31+
TypeInferenceMatchers.isSubtypeOf("property"));
32+
33+
private final TypePredicateContext typePredicateContext;
34+
35+
public QualifiedExpressionCalculator(TypePredicateContext typePredicateContext) {
36+
this.typePredicateContext = typePredicateContext;
37+
}
38+
39+
public PythonType calculate(QualifiedExpression qualifiedExpression) {
40+
Name name = qualifiedExpression.name();
41+
return Optional.of(qualifiedExpression.qualifier())
42+
.map(Expression::typeV2)
43+
.flatMap(t -> t.resolveMember(name.name()))
44+
.map(this::handlePropertyFunction)
45+
.orElse(PythonType.UNKNOWN);
46+
}
47+
48+
private PythonType handlePropertyFunction(PythonType type) {
49+
// If a member access is a method with a "property" annotation, we consider the resulting type to be the return type of the method
50+
if (type instanceof FunctionType functionType && hasFunctionPropertyDecorator(functionType)) {
51+
return functionType.returnType();
52+
}
53+
return type;
54+
}
55+
56+
private boolean hasFunctionPropertyDecorator(FunctionType functionType) {
57+
return functionType.decorators().stream().anyMatch(t -> isProperty(t.type()));
58+
}
59+
60+
private boolean isProperty(PythonType type) {
61+
return IS_PROPERTY_TYPE.evaluate(type, typePredicateContext).isTrue();
62+
}
63+
}

python-frontend/src/main/java/org/sonar/python/types/v2/matchers/IsObjectSubtypeOfPredicate.java renamed to python-frontend/src/main/java/org/sonar/python/types/v2/matchers/IsSubtypeOfPredicate.java

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,39 +19,30 @@
1919
import java.util.Set;
2020
import org.sonar.plugins.python.api.TriBool;
2121
import org.sonar.plugins.python.api.types.v2.PythonType;
22+
import org.sonar.plugins.python.api.types.v2.UnknownType;
2223

2324
import static org.sonar.python.types.v2.TypeUtils.collectTypes;
2425

25-
public class IsObjectSubtypeOfPredicate implements TypePredicate {
26+
public class IsSubtypeOfPredicate implements TypePredicate {
27+
String fullyQualifiedName;
2628

27-
private final IsObjectSatisfyingPredicate isObjectSatisfyingPredicate;
28-
29-
public IsObjectSubtypeOfPredicate(String fullyQualifiedName) {
30-
this.isObjectSatisfyingPredicate = new IsObjectSatisfyingPredicate(new IsSubtypeOfPredicate(fullyQualifiedName));
29+
public IsSubtypeOfPredicate(String fullyQualifiedName) {
30+
this.fullyQualifiedName = fullyQualifiedName;
3131
}
3232

3333
@Override
3434
public TriBool check(PythonType type, TypePredicateContext ctx) {
35-
return isObjectSatisfyingPredicate.check(type, ctx);
36-
}
35+
PythonType expectedType = ctx.typeTable().getType(fullyQualifiedName);
3736

38-
private static class IsSubtypeOfPredicate implements TypePredicate {
39-
String fullyQualifiedName;
40-
41-
public IsSubtypeOfPredicate(String fullyQualifiedName) {
42-
this.fullyQualifiedName = fullyQualifiedName;
37+
if (type instanceof UnknownType || expectedType instanceof UnknownType) {
38+
return TriBool.UNKNOWN;
4339
}
4440

45-
@Override
46-
public TriBool check(PythonType type, TypePredicateContext ctx) {
47-
PythonType expectedType = ctx.typeTable().getType(fullyQualifiedName);
48-
Set<PythonType> types = collectTypes(type);
49-
if (types.stream().anyMatch(t -> t.equals(expectedType))) {
50-
return TriBool.TRUE;
51-
} else {
52-
return TriBool.FALSE;
53-
}
41+
Set<PythonType> types = collectTypes(type);
42+
if (types.stream().anyMatch(t -> t.equals(expectedType))) {
43+
return TriBool.TRUE;
44+
} else {
45+
return TriBool.FALSE;
5446
}
5547
}
5648
}
57-

python-frontend/src/test/java/org/sonar/python/semantic/v2/TypeInferenceV2Test.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
import org.sonar.python.types.v2.LazyTypeWrapper;
9393

9494
import static org.assertj.core.api.Assertions.assertThat;
95+
import static org.assertj.core.api.InstanceOfAssertFactories.type;
9596
import static org.sonar.python.PythonTestUtils.getFirstDescendant;
9697
import static org.sonar.python.PythonTestUtils.parse;
9798
import static org.sonar.python.PythonTestUtils.parseWithoutSymbols;
@@ -1964,6 +1965,28 @@ def foo(self, x):
19641965
assertThat(discardCall.callee().typeV2()).isEqualTo(PythonType.UNKNOWN);
19651966
}
19661967

1968+
@Test
1969+
void test_ast_based_type_inference_when_function_has_no_local_variables() {
1970+
FileInput fileInput = inferTypes("""
1971+
from flask import request
1972+
def test():
1973+
request.args.get('hostname')
1974+
try: ...
1975+
except: ...
1976+
""");
1977+
1978+
Name getName = PythonTestUtils.getFirstChild(fileInput, tree -> tree instanceof Name name && "get".equals(name.name()));
1979+
1980+
assertThat(getName.typeV2())
1981+
.asInstanceOf(type(UnionType.class))
1982+
.extracting(UnionType::candidates, InstanceOfAssertFactories.SET)
1983+
.isNotEmpty()
1984+
.allSatisfy(type -> assertThat(type)
1985+
.asInstanceOf(type(FunctionType.class))
1986+
.extracting(FunctionType::name)
1987+
.isEqualTo("get"));
1988+
}
1989+
19671990
@Test
19681991
void flow_insensitive_when_try_except_with_qualified_expression() {
19691992
FileInput fileInput = inferTypes("""

0 commit comments

Comments
 (0)