Skip to content

Commit 9f8560a

Browse files
SONARPY-2170 Support detailed return types in Descriptor model (#2028)
1 parent 6b3ee04 commit 9f8560a

19 files changed

+407
-11
lines changed

python-checks/src/test/resources/checks/nonCallableCalled.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,14 @@ def fp_typed_dict():
388388
# TypedDict is defined as "TypedDict: object" in typing_extensions.pyi
389389
# Despite actually being a function
390390
x = TypedDict('x', {'a': int, 'b': str}) # Noncompliant
391+
392+
393+
def function_type_is_callable():
394+
import unittest
395+
# unittest.skip() returns a Callable
396+
unittest.skip("reason")() # OK
397+
398+
399+
def object_typevar():
400+
scheduled = []
401+
scheduled.pop()() # OK

python-checks/src/test/resources/checks/use_of_empty_return_value.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,13 @@ def import_in_different_branch():
6767
import fcntl
6868
def lock():
6969
ret = fcntl.flock(..., ...) # Noncompliant
70+
71+
72+
73+
def smth():
74+
import sys
75+
options = trial.Options()
76+
options.parseOptions(["--coverage"])
77+
self.addCleanup(sys.settrace, sys.gettrace())
78+
self.assertEqual(sys.gettrace(), options.tracer.globaltrace)
79+

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,18 @@ public class FunctionDescriptor implements Descriptor {
3939
private final LocationInFile definitionLocation;
4040
@Nullable
4141
private final String annotatedReturnTypeName;
42+
@Nullable
43+
private final TypeAnnotationDescriptor typeAnnotationDescriptor;
44+
4245

4346
public FunctionDescriptor(String name, @Nullable String fullyQualifiedName, List<Parameter> parameters, boolean isAsynchronous,
4447
boolean isInstanceMethod, List<String> decorators, boolean hasDecorators, @Nullable LocationInFile definitionLocation, @Nullable String annotatedReturnTypeName) {
48+
this(name, fullyQualifiedName, parameters, isAsynchronous, isInstanceMethod, decorators, hasDecorators, definitionLocation, annotatedReturnTypeName, null);
49+
}
50+
51+
public FunctionDescriptor(String name, @Nullable String fullyQualifiedName, List<Parameter> parameters, boolean isAsynchronous,
52+
boolean isInstanceMethod, List<String> decorators, boolean hasDecorators, @Nullable LocationInFile definitionLocation,
53+
@Nullable String annotatedReturnTypeName, @Nullable TypeAnnotationDescriptor typeAnnotationDescriptor) {
4554

4655
this.name = name;
4756
this.fullyQualifiedName = fullyQualifiedName;
@@ -52,6 +61,7 @@ public FunctionDescriptor(String name, @Nullable String fullyQualifiedName, List
5261
this.hasDecorators = hasDecorators;
5362
this.definitionLocation = definitionLocation;
5463
this.annotatedReturnTypeName = annotatedReturnTypeName;
64+
this.typeAnnotationDescriptor = typeAnnotationDescriptor;
5565
}
5666

5767
@Override
@@ -99,6 +109,11 @@ public String annotatedReturnTypeName() {
99109
return annotatedReturnTypeName;
100110
}
101111

112+
@CheckForNull
113+
public TypeAnnotationDescriptor typeAnnotationDescriptor() {
114+
return typeAnnotationDescriptor;
115+
}
116+
102117
public static class Parameter {
103118

104119
private final String name;
@@ -172,6 +187,7 @@ public static class FunctionDescriptorBuilder {
172187
private boolean hasDecorators = false;
173188
private LocationInFile definitionLocation = null;
174189
private String annotatedReturnTypeName = null;
190+
private TypeAnnotationDescriptor typeAnnotationDescriptor = null;
175191

176192
public FunctionDescriptorBuilder withName(String name) {
177193
this.name = name;
@@ -218,9 +234,14 @@ public FunctionDescriptorBuilder withAnnotatedReturnTypeName(@Nullable String an
218234
return this;
219235
}
220236

237+
public FunctionDescriptorBuilder withTypeAnnotationDescriptor(@Nullable TypeAnnotationDescriptor typeAnnotationDescriptor) {
238+
this.typeAnnotationDescriptor = typeAnnotationDescriptor;
239+
return this;
240+
}
241+
221242
public FunctionDescriptor build() {
222243
return new FunctionDescriptor(name, fullyQualifiedName, parameters, isAsynchronous, isInstanceMethod, decorators,
223-
hasDecorators, definitionLocation, annotatedReturnTypeName);
244+
hasDecorators, definitionLocation, annotatedReturnTypeName, typeAnnotationDescriptor);
224245
}
225246
}
226247
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2024 SonarSource SA
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 GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.index;
21+
22+
import java.util.List;
23+
import javax.annotation.CheckForNull;
24+
import javax.annotation.Nullable;
25+
26+
public class TypeAnnotationDescriptor {
27+
28+
String prettyPrintedName;
29+
TypeKind kind;
30+
List<TypeAnnotationDescriptor> args;
31+
String fullyQualifiedName;
32+
33+
public TypeAnnotationDescriptor(String prettyPrintedName, TypeKind kind, List<TypeAnnotationDescriptor> args, @Nullable String fullyQualifiedName) {
34+
this.prettyPrintedName = prettyPrintedName;
35+
this.kind = kind;
36+
this.args = args;
37+
this.fullyQualifiedName = fullyQualifiedName;
38+
}
39+
40+
public String prettyPrintedName() {
41+
return prettyPrintedName;
42+
}
43+
44+
public TypeKind kind() {
45+
return kind;
46+
}
47+
48+
public List<TypeAnnotationDescriptor> args() {
49+
return args;
50+
}
51+
52+
@CheckForNull
53+
public String fullyQualifiedName() {
54+
return fullyQualifiedName;
55+
}
56+
57+
public enum TypeKind {
58+
INSTANCE,
59+
UNION,
60+
TYPE,
61+
TUPLE,
62+
TYPE_VAR,
63+
ANY,
64+
NONE,
65+
TYPE_ALIAS,
66+
CALLABLE,
67+
LITERAL,
68+
UNINHABITED,
69+
UNBOUND,
70+
TYPED_DICT
71+
}
72+
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@
3232
public class FunctionDescriptorToPythonTypeConverter implements DescriptorToPythonTypeConverter {
3333

3434
private final ParameterConverter parameterConverter;
35+
private final TypeAnnotationToPythonTypeConverter typeAnnotationConverter;
3536

3637
public FunctionDescriptorToPythonTypeConverter() {
3738
parameterConverter = new ParameterConverter();
39+
typeAnnotationConverter = new TypeAnnotationToPythonTypeConverter();
3840
}
3941

4042
public PythonType convert(ConversionContext ctx, FunctionDescriptor from) {
@@ -43,8 +45,8 @@ public PythonType convert(ConversionContext ctx, FunctionDescriptor from) {
4345
.map(parameter -> parameterConverter.convert(ctx, parameter))
4446
.toList();
4547

46-
var returnType = Optional.ofNullable(from.annotatedReturnTypeName())
47-
.map(fqn -> (PythonType) ctx.lazyTypesContext().getOrCreateLazyType(fqn))
48+
PythonType returnType = Optional.ofNullable(from.typeAnnotationDescriptor())
49+
.map(typeAnnotation -> typeAnnotationConverter.convert(ctx, typeAnnotation))
4850
.map(TypeUtils::ensureWrappedObjectType)
4951
.orElse(PythonType.UNKNOWN);
5052

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2024 SonarSource SA
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 GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.semantic.v2.converter;
21+
22+
import java.util.Optional;
23+
import java.util.Set;
24+
import java.util.function.Predicate;
25+
import java.util.stream.Collectors;
26+
import org.sonar.python.index.TypeAnnotationDescriptor;
27+
import org.sonar.python.types.v2.LazyUnionType;
28+
import org.sonar.python.types.v2.PythonType;
29+
30+
public class TypeAnnotationToPythonTypeConverter {
31+
32+
public PythonType convert(ConversionContext context, TypeAnnotationDescriptor type) {
33+
switch (type.kind()) {
34+
case INSTANCE:
35+
String fullyQualifiedName = type.fullyQualifiedName();
36+
if (fullyQualifiedName == null) {
37+
return PythonType.UNKNOWN;
38+
}
39+
// _SpecialForm is the type used for some special types, like Callable, Union, TypeVar, ...
40+
// It comes from CPython impl: https://github.com/python/cpython/blob/e39ae6bef2c357a88e232dcab2e4b4c0f367544b/Lib/typing.py#L439
41+
// This doesn't seem to be very precisely specified in typeshed, because it has special semantic.
42+
// To avoid FPs, we treat it as ANY
43+
if ("typing._SpecialForm".equals(fullyQualifiedName)) {
44+
return PythonType.UNKNOWN;
45+
}
46+
return fullyQualifiedName.isEmpty() ? PythonType.UNKNOWN : context.lazyTypesContext().getOrCreateLazyType(fullyQualifiedName);
47+
case TYPE:
48+
return context.lazyTypesContext().getOrCreateLazyType("type");
49+
case TYPE_ALIAS:
50+
return convert(context, type.args().get(0));
51+
case CALLABLE:
52+
// this should be handled as a function type - see SONARPY-953
53+
return context.lazyTypesContext().getOrCreateLazyType("function");
54+
case UNION:
55+
return new LazyUnionType(type.args().stream().map(t -> convert(context, t)).collect(Collectors.toSet()));
56+
case TUPLE:
57+
return context.lazyTypesContext().getOrCreateLazyType("tuple");
58+
case NONE:
59+
return context.lazyTypesContext().getOrCreateLazyType("NoneType");
60+
case TYPED_DICT:
61+
// SONARPY-2179: This case only makes sense for parameter types, which are not supported yet
62+
return context.lazyTypesContext().getOrCreateLazyType("dict");
63+
case TYPE_VAR:
64+
return Optional.of(type)
65+
.filter(TypeAnnotationToPythonTypeConverter::filterTypeVar)
66+
.map(TypeAnnotationDescriptor::fullyQualifiedName)
67+
.map(context.lazyTypesContext()::getOrCreateLazyType)
68+
.map(PythonType.class::cast)
69+
.orElse(PythonType.UNKNOWN);
70+
default:
71+
return PythonType.UNKNOWN;
72+
}
73+
}
74+
75+
private static final Set<String> EXCLUDING_TYPE_VAR_FQN_PATTERNS = Set.of(
76+
"object",
77+
"^builtins\\.object$",
78+
// ref: SONARPY-1477
79+
"^_ctypes\\._CanCastTo$");
80+
81+
public static boolean filterTypeVar(TypeAnnotationDescriptor type) {
82+
return Optional.of(type)
83+
// Filtering self returning methods until the SONARPY-1472 will be solved
84+
.filter(Predicate.not(t -> t.prettyPrintedName().endsWith(".Self")))
85+
.map(TypeAnnotationDescriptor::fullyQualifiedName)
86+
.filter(Predicate.not(String::isEmpty))
87+
// We ignore TypeVar referencing "builtins.object" or "object" to avoid false positives
88+
.filter(fqn -> EXCLUDING_TYPE_VAR_FQN_PATTERNS.stream().noneMatch(fqn::matches))
89+
.isPresent();
90+
}
91+
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,17 @@
2222
import java.util.Collection;
2323
import java.util.Optional;
2424
import org.sonar.python.index.FunctionDescriptor;
25+
import org.sonar.python.index.TypeAnnotationDescriptor;
2526
import org.sonar.python.types.protobuf.SymbolsProtos;
2627

2728
public class FunctionSymbolToDescriptorConverter {
2829

2930
private final ParameterSymbolToDescriptorConverter parameterConverter;
31+
private final TypeSymbolToDescriptorConverter typeConverter;
3032

3133
public FunctionSymbolToDescriptorConverter() {
3234
parameterConverter = new ParameterSymbolToDescriptorConverter();
35+
typeConverter = new TypeSymbolToDescriptorConverter();
3336
}
3437

3538
public FunctionDescriptor convert(SymbolsProtos.FunctionSymbol functionSymbol) {
@@ -38,7 +41,12 @@ public FunctionDescriptor convert(SymbolsProtos.FunctionSymbol functionSymbol) {
3841

3942
public FunctionDescriptor convert(SymbolsProtos.FunctionSymbol functionSymbol, boolean isParentIsAClass) {
4043
var fullyQualifiedName = TypeShedUtils.normalizedFqn(functionSymbol.getFullyQualifiedName());
41-
var returnType = TypeShedUtils.getTypesNormalizedFqn(functionSymbol.getReturnAnnotation());
44+
TypeAnnotationDescriptor typeAnnotationDescriptor = null;
45+
if (functionSymbol.hasReturnAnnotation()) {
46+
SymbolsProtos.Type returnAnnotation = functionSymbol.getReturnAnnotation();
47+
typeAnnotationDescriptor = typeConverter.convert(returnAnnotation);
48+
}
49+
String returnType = TypeShedUtils.getTypesNormalizedFqn(functionSymbol.getReturnAnnotation());
4250
var decorators = Optional.of(functionSymbol)
4351
.map(SymbolsProtos.FunctionSymbol::getResolvedDecoratorNamesList)
4452
.stream()
@@ -56,9 +64,9 @@ public FunctionDescriptor convert(SymbolsProtos.FunctionSymbol functionSymbol, b
5664
.withIsInstanceMethod(isInstanceMethod)
5765
.withHasDecorators(functionSymbol.getHasDecorators())
5866
.withAnnotatedReturnTypeName(returnType)
67+
.withTypeAnnotationDescriptor(typeAnnotationDescriptor)
5968
.withDecorators(decorators)
6069
.withParameters(parameters)
6170
.build();
6271
}
63-
6472
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2024 SonarSource SA
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 GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonar.python.semantic.v2.typeshed;
21+
22+
import java.util.List;
23+
import org.sonar.python.index.TypeAnnotationDescriptor;
24+
import org.sonar.python.types.protobuf.SymbolsProtos;
25+
26+
public class TypeSymbolToDescriptorConverter {
27+
28+
TypeAnnotationDescriptor convert(SymbolsProtos.Type type) {
29+
List<TypeAnnotationDescriptor> args = type.getArgsList().stream()
30+
.map(this::convert)
31+
.toList();
32+
TypeAnnotationDescriptor.TypeKind kind = TypeAnnotationDescriptor.TypeKind.valueOf(type.getKind().name());
33+
String normalizedFqn = TypeShedUtils.normalizedFqn(type.getFullyQualifiedName());
34+
return new TypeAnnotationDescriptor(
35+
type.getPrettyPrintedName(),
36+
kind,
37+
args,
38+
normalizedFqn);
39+
}
40+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ public TriBool instancesHaveMember(String memberName) {
188188
// TODO: instances of NamedTuple are type
189189
return TriBool.TRUE;
190190
}
191+
if ("function".equals(this.name) && "__call__".equals(memberName)) {
192+
// __call__ is not formally defined on function, but is present
193+
return TriBool.TRUE;
194+
}
191195
return resolveMember(memberName).isPresent() ? TriBool.TRUE : TriBool.FALSE;
192196
}
193197

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import org.sonar.plugins.python.api.LocationInFile;
2727
import org.sonar.python.semantic.v2.LazyTypesContext;
2828

29-
public class LazyType implements PythonType {
29+
public class LazyType implements PythonType, ResolvableType {
3030

3131
String fullyQualifiedName;
3232
private final Queue<Consumer<PythonType>> consumers;

0 commit comments

Comments
 (0)