Skip to content

Commit a893b16

Browse files
SONARPY-1786 Add basic information to locally defined ClassType (parents and metaclass)
1 parent f925834 commit a893b16

File tree

12 files changed

+928
-12
lines changed

12 files changed

+928
-12
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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;
21+
22+
import java.util.ArrayList;
23+
import java.util.HashSet;
24+
import java.util.List;
25+
import java.util.Set;
26+
import org.sonar.python.types.v2.ClassType;
27+
import org.sonar.python.types.v2.Member;
28+
import org.sonar.python.types.v2.PythonType;
29+
30+
public class ClassTypeBuilder implements TypeBuilder<ClassType> {
31+
32+
33+
String name;
34+
Set<Member> members = new HashSet<>();
35+
List<PythonType> attributes = new ArrayList<>();
36+
List<PythonType> superClasses = new ArrayList<>();
37+
List<PythonType> metaClasses = new ArrayList<>();
38+
39+
@Override
40+
public ClassType build() {
41+
return new ClassType(name, members, attributes, superClasses, metaClasses);
42+
}
43+
44+
public ClassTypeBuilder setName(String name) {
45+
this.name = name;
46+
return this;
47+
}
48+
49+
public List<PythonType> superClasses() {
50+
return superClasses;
51+
}
52+
53+
public List<PythonType> metaClasses() {
54+
return metaClasses;
55+
}
56+
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ private static PythonType convertToObjectType(Symbol symbol) {
100100
}
101101

102102
private static PythonType convertToFunctionType(FunctionSymbol symbol, Map<Symbol, PythonType> createdTypesBySymbol) {
103-
// TODO: ensure we don't build twice
104103
if (createdTypesBySymbol.containsKey(symbol)) {
105104
return createdTypesBySymbol.get(symbol);
106105
}
@@ -112,8 +111,7 @@ private static PythonType convertToFunctionType(FunctionSymbol symbol, Map<Symbo
112111
.withAsynchronous(false)
113112
.withHasDecorators(false)
114113
.withInstanceMethod(false)
115-
.withHasVariadicParameter(false)
116-
.withOwner(null);
114+
.withHasVariadicParameter(false);
117115
FunctionType functionType = functionTypeBuilder.build();
118116
createdTypesBySymbol.put(symbol, functionType);
119117
return functionType;

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

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.List;
2626
import java.util.Optional;
2727
import javax.annotation.Nullable;
28+
import org.sonar.plugins.python.api.tree.ArgList;
2829
import org.sonar.plugins.python.api.tree.AssignmentStatement;
2930
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
3031
import org.sonar.plugins.python.api.tree.ClassDef;
@@ -37,13 +38,16 @@
3738
import org.sonar.plugins.python.api.tree.ImportName;
3839
import org.sonar.plugins.python.api.tree.ListLiteral;
3940
import org.sonar.plugins.python.api.tree.Name;
41+
import org.sonar.plugins.python.api.tree.NoneExpression;
4042
import org.sonar.plugins.python.api.tree.NumericLiteral;
4143
import org.sonar.plugins.python.api.tree.QualifiedExpression;
44+
import org.sonar.plugins.python.api.tree.RegularArgument;
4245
import org.sonar.plugins.python.api.tree.StringLiteral;
4346
import org.sonar.plugins.python.api.tree.Tree;
4447
import org.sonar.plugins.python.api.types.InferredType;
4548
import org.sonar.python.tree.ListLiteralImpl;
4649
import org.sonar.python.tree.NameImpl;
50+
import org.sonar.python.tree.NoneExpressionImpl;
4751
import org.sonar.python.tree.NumericLiteralImpl;
4852
import org.sonar.python.tree.StringLiteralImpl;
4953
import org.sonar.python.types.RuntimeType;
@@ -64,6 +68,8 @@ public TypeInferenceV2(ProjectLevelTypeTable projectLevelTypeTable) {
6468
this.projectLevelTypeTable = projectLevelTypeTable;
6569
}
6670

71+
private static final String BUILTINS = "builtins";
72+
6773
@Override
6874
public void visitFileInput(FileInput fileInput) {
6975
var type = new ModuleType("somehow get its name");
@@ -72,24 +78,31 @@ public void visitFileInput(FileInput fileInput) {
7278

7379
@Override
7480
public void visitStringLiteral(StringLiteral stringLiteral) {
75-
ModuleType builtins = this.projectLevelTypeTable.getModule("builtins");
81+
ModuleType builtins = this.projectLevelTypeTable.getModule(BUILTINS);
7682
// TODO: multiple object types to represent str instance?
7783
((StringLiteralImpl) stringLiteral).typeV2(new ObjectType(builtins.resolveMember("str"), List.of(), List.of()));
7884
}
7985

8086
@Override
8187
public void visitNumericLiteral(NumericLiteral numericLiteral) {
82-
ModuleType builtins = this.projectLevelTypeTable.getModule("builtins");
88+
ModuleType builtins = this.projectLevelTypeTable.getModule(BUILTINS);
8389
InferredType type = numericLiteral.type();
8490
String memberName = ((RuntimeType) type).getTypeClass().fullyQualifiedName();
8591
if (memberName != null) {
8692
((NumericLiteralImpl) numericLiteral).typeV2(new ObjectType(builtins.resolveMember(memberName), List.of(), List.of()));
8793
}
8894
}
8995

96+
@Override
97+
public void visitNone(NoneExpression noneExpression) {
98+
ModuleType builtins = this.projectLevelTypeTable.getModule(BUILTINS);
99+
// TODO: multiple object types to represent str instance?
100+
((NoneExpressionImpl) noneExpression).typeV2(new ObjectType(builtins.resolveMember("NoneType"), List.of(), List.of()));
101+
}
102+
90103
@Override
91104
public void visitListLiteral(ListLiteral listLiteral) {
92-
ModuleType builtins = this.projectLevelTypeTable.getModule("builtins");
105+
ModuleType builtins = this.projectLevelTypeTable.getModule(BUILTINS);
93106
scan(listLiteral.elements());
94107
List<PythonType> pythonTypes = listLiteral.elements().expressions().stream().map(Expression::typeV2).distinct().toList();
95108
// TODO: cleanly reduce attributes
@@ -100,12 +113,50 @@ public void visitListLiteral(ListLiteral listLiteral) {
100113
public void visitClassDef(ClassDef classDef) {
101114
scan(classDef.args());
102115
Name name = classDef.name();
103-
ClassType type = new ClassType(name.name());
116+
ClassTypeBuilder classTypeBuilder = new ClassTypeBuilder().setName(name.name());
117+
resolveTypeHierarchy(classDef, classTypeBuilder);
118+
ClassType type = classTypeBuilder.build();
104119
((NameImpl) name).typeV2(type);
105120

106121
inTypeScope(type, () -> scan(classDef.body()));
107122
}
108123

124+
static void resolveTypeHierarchy(ClassDef classDef, ClassTypeBuilder classTypeBuilder) {
125+
Optional.of(classDef)
126+
.map(ClassDef::args)
127+
.map(ArgList::arguments)
128+
.stream()
129+
.flatMap(Collection::stream)
130+
.forEach(argument -> {
131+
if (argument instanceof RegularArgument regularArgument) {
132+
addParentClass(classTypeBuilder, regularArgument);
133+
} else {
134+
classTypeBuilder.superClasses().add(PythonType.UNKNOWN);
135+
}
136+
});
137+
}
138+
139+
private static void addParentClass(ClassTypeBuilder classTypeBuilder, RegularArgument regularArgument) {
140+
Name keyword = regularArgument.keywordArgument();
141+
// TODO: store names if not resolved properly
142+
if (keyword != null) {
143+
if ("metaclass".equals(keyword.name())) {
144+
PythonType argumentType = getTypeV2FromArgument(regularArgument);
145+
classTypeBuilder.metaClasses().add(argumentType);
146+
}
147+
return;
148+
}
149+
PythonType argumentType = getTypeV2FromArgument(regularArgument);
150+
classTypeBuilder.superClasses().add(argumentType);
151+
// TODO: handle generics
152+
}
153+
154+
private static PythonType getTypeV2FromArgument(RegularArgument regularArgument) {
155+
Expression expression = regularArgument.expression();
156+
// Ensure we support correctly typing symbols like "List[str] / list[str]"
157+
return expression.typeV2();
158+
}
159+
109160
@Override
110161
public void visitFunctionDef(FunctionDef functionDef) {
111162
scan(functionDef.decorators());
@@ -129,7 +180,7 @@ private FunctionType buildFunctionType(FunctionDef functionDef) {
129180
owner = classType;
130181
}
131182
if (owner != null) {
132-
functionTypeBuilder.setOwner(owner);
183+
functionTypeBuilder.withOwner(owner);
133184
}
134185
FunctionType functionType = functionTypeBuilder.build();
135186
if (owner != null) {

python-frontend/src/main/java/org/sonar/python/tree/NoneExpressionImpl.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
import org.sonar.plugins.python.api.tree.TreeVisitor;
2828
import org.sonar.plugins.python.api.types.InferredType;
2929
import org.sonar.python.types.InferredTypes;
30+
import org.sonar.python.types.v2.PythonType;
3031

3132
public class NoneExpressionImpl extends PyTree implements NoneExpression {
3233
private final Token none;
34+
PythonType pythonType;
3335

3436
public NoneExpressionImpl(Token none) {
3537
this.none = none;
@@ -59,4 +61,13 @@ public Kind getKind() {
5961
public InferredType type() {
6062
return InferredTypes.NONE;
6163
}
64+
65+
public void typeV2(PythonType pythonType) {
66+
this.pythonType = pythonType;
67+
}
68+
69+
@Override
70+
public PythonType typeV2() {
71+
return pythonType;
72+
}
6273
}

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

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@ public record ClassType(
3535
String name,
3636
Set<Member> members,
3737
List<PythonType> attributes,
38-
List<PythonType> superClasses) implements PythonType {
38+
List<PythonType> superClasses,
39+
List<PythonType> metaClasses) implements PythonType {
3940

4041
public ClassType(String name) {
41-
this(name, new HashSet<>(), new ArrayList<>(), new ArrayList<>());
42+
this(name, new HashSet<>(), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
4243
}
4344

4445
public ClassType(String name, List<PythonType> attributes) {
45-
this(name, new HashSet<>(), attributes, new ArrayList<>());
46+
this(name, new HashSet<>(), attributes, new ArrayList<>(), new ArrayList<>());
4647
}
4748

4849
@Override
@@ -101,4 +102,61 @@ public PythonType resolveMember(String memberName) {
101102
.map(Member::type)
102103
.findFirst().orElse(PythonType.UNKNOWN);
103104
}
105+
106+
public boolean hasUnresolvedHierarchy() {
107+
return superClasses.stream().anyMatch(s -> {
108+
if (s instanceof ClassType parentClassType) {
109+
return parentClassType.hasUnresolvedHierarchy();
110+
}
111+
return true;
112+
}
113+
);
114+
}
115+
116+
@Override
117+
public TriBool hasMember(String memberName) {
118+
// a ClassType is an object of class type, it has the same members as those present on any type
119+
if ("__call__".equals(memberName)) {
120+
return TriBool.TRUE;
121+
}
122+
if (hasUnresolvedHierarchy()) {
123+
return TriBool.UNKNOWN;
124+
}
125+
// TODO: Not correct, we should look at what the actual type is instead (SONARPY-1666)
126+
return TriBool.UNKNOWN;
127+
}
128+
129+
public boolean hasMetaClass() {
130+
return !this.metaClasses.isEmpty();
131+
}
132+
133+
public TriBool instancesHaveMember(String memberName) {
134+
if (hasUnresolvedHierarchy() || hasMetaClass()) {
135+
return TriBool.UNKNOWN;
136+
}
137+
if ("NamedTuple".equals(this.name)) {
138+
// TODO: instances of NamedTuple are type
139+
return TriBool.TRUE;
140+
}
141+
// TODO: look at parents
142+
return resolveMember(memberName) != PythonType.UNKNOWN ? TriBool.TRUE : TriBool.FALSE;
143+
}
144+
145+
@Override
146+
public boolean equals(Object o) {
147+
if (this == o) return true;
148+
if (o == null || getClass() != o.getClass()) return false;
149+
ClassType classType = (ClassType) o;
150+
boolean haveSameAttributes = Objects.equals(name, classType.name) && Objects.equals(members, classType.members) && Objects.equals(attributes, classType.attributes);
151+
List<String> parentNames = superClasses.stream().map(PythonType::key).toList();
152+
List<String> metaClassNames = metaClasses.stream().map(PythonType::key).toList();
153+
List<String> otherParentNames = classType.superClasses.stream().map(PythonType::key).toList();
154+
List<String> otherMetaClassNames = classType.metaClasses.stream().map(PythonType::key).toList();
155+
return haveSameAttributes && Objects.equals(parentNames, otherParentNames) && Objects.equals(metaClassNames, otherMetaClassNames);
156+
}
157+
158+
@Override
159+
public int hashCode() {
160+
return Objects.hash(name, members, attributes, superClasses);
161+
}
104162
}

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.sonar.python.types.v2;
2121

2222
import java.util.List;
23+
import java.util.Objects;
2324
import javax.annotation.Nullable;
2425

2526
/**
@@ -35,4 +36,25 @@ public record FunctionType(
3536
boolean isInstanceMethod,
3637
boolean hasVariadicParameter,
3738
@Nullable PythonType owner
38-
) implements PythonType { }
39+
) implements PythonType {
40+
41+
@Override
42+
public boolean equals(Object o) {
43+
if (this == o) return true;
44+
if (o == null || getClass() != o.getClass()) return false;
45+
FunctionType that = (FunctionType) o;
46+
return hasDecorators == that.hasDecorators
47+
&& isAsynchronous == that.isAsynchronous
48+
&& isInstanceMethod == that.isInstanceMethod
49+
&& hasVariadicParameter == that.hasVariadicParameter
50+
&& Objects.equals(name, that.name)
51+
&& Objects.equals(returnType, that.returnType)
52+
&& Objects.equals(attributes, that.attributes)
53+
&& Objects.equals(parameters, that.parameters);
54+
}
55+
56+
@Override
57+
public int hashCode() {
58+
return Objects.hash(name, attributes, parameters, returnType, isAsynchronous, hasDecorators, isInstanceMethod, hasVariadicParameter);
59+
}
60+
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,15 @@ public PythonType resolveMember(String memberName) {
4747
.findFirst()
4848
.orElseGet(() -> type.resolveMember(memberName));
4949
}
50+
51+
@Override
52+
public TriBool hasMember(String memberName) {
53+
if (resolveMember(memberName) != PythonType.UNKNOWN) {
54+
return TriBool.TRUE;
55+
}
56+
if (type instanceof ClassType classType) {
57+
return classType.instancesHaveMember(memberName);
58+
}
59+
return TriBool.UNKNOWN;
60+
}
5061
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,7 @@ default PythonType resolveMember(String memberName) {
4545
return PythonType.UNKNOWN;
4646
}
4747

48+
default TriBool hasMember(String memberName) {
49+
return TriBool.UNKNOWN;
50+
}
4851
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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.types.v2;
21+
22+
public enum TriBool {
23+
TRUE,
24+
FALSE,
25+
UNKNOWN
26+
}

0 commit comments

Comments
 (0)