Skip to content

Commit 61347bd

Browse files
SONARPY-2131 Support LazyType for super classes of ClassType
1 parent 373d797 commit 61347bd

File tree

11 files changed

+185
-35
lines changed

11 files changed

+185
-35
lines changed

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import javax.annotation.Nullable;
2828
import org.sonar.plugins.python.api.LocationInFile;
2929
import org.sonar.python.types.v2.ClassType;
30+
import org.sonar.python.types.v2.LazyTypeWrapper;
31+
import org.sonar.python.types.v2.TypeWrapper;
3032
import org.sonar.python.types.v2.Member;
3133
import org.sonar.python.types.v2.PythonType;
3234

@@ -36,7 +38,7 @@ public class ClassTypeBuilder implements TypeBuilder<ClassType> {
3638
String name;
3739
Set<Member> members = new HashSet<>();
3840
List<PythonType> attributes = new ArrayList<>();
39-
List<PythonType> superClasses = new ArrayList<>();
41+
List<TypeWrapper> superClasses = new ArrayList<>();
4042
List<PythonType> metaClasses = new ArrayList<>();
4143
boolean hasDecorators = false;
4244
LocationInFile definitionLocation;
@@ -62,12 +64,17 @@ public ClassTypeBuilder withDefinitionLocation(@Nullable LocationInFile definiti
6264
return this;
6365
}
6466

65-
public List<PythonType> superClasses() {
67+
public List<TypeWrapper> superClasses() {
6668
return superClasses;
6769
}
6870

71+
public ClassTypeBuilder addSuperClass(PythonType type) {
72+
superClasses.add(new LazyTypeWrapper(type));
73+
return this;
74+
}
75+
6976
public ClassTypeBuilder withSuperClasses(PythonType... types) {
70-
superClasses.addAll(Arrays.asList(types));
77+
Arrays.stream(types).forEach(this::addSuperClass);
7178
return this;
7279
}
7380

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

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@
3838
import org.sonar.python.semantic.ClassSymbolImpl;
3939
import org.sonar.python.semantic.FunctionSymbolImpl;
4040
import org.sonar.python.semantic.ProjectLevelSymbolTable;
41-
import org.sonar.python.semantic.SymbolImpl;
4241
import org.sonar.python.types.InferredTypes;
4342
import org.sonar.python.types.protobuf.SymbolsProtos;
4443
import org.sonar.python.types.v2.ClassType;
4544
import org.sonar.python.types.v2.FunctionType;
4645
import org.sonar.python.types.v2.LazyType;
46+
import org.sonar.python.types.v2.LazyTypeWrapper;
4747
import org.sonar.python.types.v2.Member;
4848
import org.sonar.python.types.v2.ModuleType;
4949
import org.sonar.python.types.v2.ParameterV2;
@@ -184,19 +184,19 @@ private PythonType convertToClassType(ClassSymbol symbol, Map<Symbol, PythonType
184184
symbol.declaredMembers().stream().map(m -> new Member(m.name(), convertToType(m, createdTypesBySymbol))).collect(Collectors.toSet());
185185
classType.members().addAll(members);
186186

187-
188187
Optional.of(symbol)
189188
.filter(ClassSymbolImpl.class::isInstance)
190189
.map(ClassSymbolImpl.class::cast)
191190
.filter(ClassSymbolImpl::shouldSearchHierarchyInTypeshed)
192191
.map(ClassSymbol::superClassesFqn)
193-
.map(fqns -> fqns.stream().map(this::typeshedSymbolWithFQN))
192+
.map(fqns -> fqns.stream().map(this::resolvePossibleLazyType))
194193
.or(() -> Optional.of(symbol)
195194
.map(ClassSymbol::superClasses)
196-
.map(Collection::stream))
195+
.map(Collection::stream)
196+
.map(symbols -> symbols.map(s -> convertToType(s, createdTypesBySymbol))))
197197
.stream()
198198
.flatMap(Function.identity())
199-
.map(s -> convertToType(s, createdTypesBySymbol))
199+
.map(LazyTypeWrapper::new)
200200
.forEach(classType.superClasses()::add);
201201

202202
return classType;
@@ -213,13 +213,6 @@ private static ParameterV2 convertParameter(FunctionSymbol.Parameter parameter)
213213
null);
214214
}
215215

216-
private Symbol typeshedSymbolWithFQN(String fullyQualifiedName) {
217-
String[] fqnSplitByDot = fullyQualifiedName.split("\\.");
218-
String localName = fqnSplitByDot[fqnSplitByDot.length - 1];
219-
Symbol symbol = typeShed.symbolWithFQN(fullyQualifiedName);
220-
return symbol == null ? new SymbolImpl(localName, fullyQualifiedName) : ((SymbolImpl) symbol).copyWithoutUsages();
221-
}
222-
223216
private PythonType convertToUnionType(AmbiguousSymbol ambiguousSymbol, Map<Symbol, PythonType> createdTypesBySymbol) {
224217
Set<PythonType> pythonTypes = ambiguousSymbol.alternatives().stream().map(a -> convertToType(a, createdTypesBySymbol)).collect(Collectors.toSet());
225218
return new UnionType(pythonTypes);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ static void resolveTypeHierarchy(ClassDef classDef, ClassTypeBuilder classTypeBu
227227
if (argument instanceof RegularArgument regularArgument) {
228228
addParentClass(classTypeBuilder, regularArgument);
229229
} else {
230-
classTypeBuilder.superClasses().add(PythonType.UNKNOWN);
230+
classTypeBuilder.addSuperClass(PythonType.UNKNOWN);
231231
}
232232
});
233233
}
@@ -243,7 +243,7 @@ private static void addParentClass(ClassTypeBuilder classTypeBuilder, RegularArg
243243
return;
244244
}
245245
PythonType argumentType = getTypeV2FromArgument(regularArgument);
246-
classTypeBuilder.superClasses().add(argumentType);
246+
classTypeBuilder.addSuperClass(argumentType);
247247
// TODO: SONARPY-1869 handle generics
248248
}
249249

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public final class ClassType implements PythonType {
4040
private final String name;
4141
private final Set<Member> members;
4242
private final List<PythonType> attributes;
43-
private final List<PythonType> superClasses;
43+
private final List<TypeWrapper> superClasses;
4444
private final List<PythonType> metaClasses;
4545
private final boolean hasDecorators;
4646
private final LocationInFile locationInFile;
@@ -49,7 +49,7 @@ public ClassType(
4949
String name,
5050
Set<Member> members,
5151
List<PythonType> attributes,
52-
List<PythonType> superClasses,
52+
List<TypeWrapper> superClasses,
5353
List<PythonType> metaClasses,
5454
boolean hasDecorators,
5555
@Nullable LocationInFile locationInFile) {
@@ -109,7 +109,7 @@ public boolean isCompatibleWith(PythonType another) {
109109

110110
@Beta
111111
public boolean isASubClassFrom(ClassType other) {
112-
return superClasses.stream().anyMatch(superClass -> superClass.isCompatibleWith(other));
112+
return superClasses().stream().anyMatch(superClass -> superClass.type().isCompatibleWith(other));
113113
}
114114

115115
@Beta
@@ -140,16 +140,16 @@ private Optional<PythonType> localMember(String memberName) {
140140
}
141141

142142
private Optional<PythonType> inheritedMember(String memberName) {
143-
return superClasses.stream()
144-
.map(s -> s.resolveMember(memberName))
143+
return superClasses().stream()
144+
.map(s -> s.type().resolveMember(memberName))
145145
.filter(Optional::isPresent)
146146
.map(Optional::get)
147147
.findFirst();
148148
}
149149

150150
public boolean hasUnresolvedHierarchy() {
151-
return superClasses.stream().anyMatch(s -> {
152-
if (s instanceof ClassType parentClassType) {
151+
return superClasses().stream().anyMatch(s -> {
152+
if (s.type() instanceof ClassType parentClassType) {
153153
return parentClassType.hasUnresolvedHierarchy();
154154
}
155155
return true;
@@ -174,6 +174,7 @@ public boolean hasMetaClass() {
174174
return !this.metaClasses.isEmpty() ||
175175
this.superClasses()
176176
.stream()
177+
.map(TypeWrapper::type)
177178
.filter(ClassType.class::isInstance)
178179
.map(ClassType.class::cast)
179180
.anyMatch(ClassType::hasMetaClass);
@@ -213,7 +214,7 @@ public List<PythonType> attributes() {
213214
return attributes;
214215
}
215216

216-
public List<PythonType> superClasses() {
217+
public List<TypeWrapper> superClasses() {
217218
return superClasses;
218219
}
219220

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 class LazyTypeWrapper implements TypeWrapper {
23+
private PythonType type;
24+
25+
public LazyTypeWrapper(PythonType type) {
26+
this.type = type;
27+
if (type instanceof LazyType lazyType) {
28+
lazyType.addConsumer(this::resolveLazyType);
29+
}
30+
}
31+
32+
public PythonType type() {
33+
return TypeUtils.resolved(this.type);
34+
}
35+
36+
public void resolveLazyType(PythonType pythonType) {
37+
if (!(type instanceof LazyType)) {
38+
throw new IllegalStateException("Trying to resolve an already resolved lazy type.");
39+
}
40+
this.type = pythonType;
41+
}
42+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ private static Set<PythonType> collectTypes(PythonType type) {
220220
result.add(PythonType.UNKNOWN);
221221
queue.clear();
222222
} else if (currentType instanceof ClassType classType) {
223-
queue.addAll(classType.superClasses());
223+
queue.addAll(classType.superClasses().stream().map(TypeWrapper::type).toList());
224224
}
225225
}
226226
return result;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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 interface TypeWrapper {
23+
24+
PythonType type();
25+
}

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import org.sonar.python.types.v2.ClassType;
5959
import org.sonar.python.types.v2.FunctionType;
6060
import org.sonar.python.types.v2.LazyType;
61+
import org.sonar.python.types.v2.TypeWrapper;
6162
import org.sonar.python.types.v2.ModuleType;
6263
import org.sonar.python.types.v2.ObjectType;
6364
import org.sonar.python.types.v2.ParameterV2;
@@ -73,6 +74,7 @@
7374
import static org.sonar.python.PythonTestUtils.parseWithoutSymbols;
7475
import static org.sonar.python.PythonTestUtils.pythonFile;
7576
import static org.sonar.python.types.v2.TypesTestUtils.DICT_TYPE;
77+
import static org.sonar.python.types.v2.TypesTestUtils.EXCEPTION_TYPE;
7678
import static org.sonar.python.types.v2.TypesTestUtils.FLOAT_TYPE;
7779
import static org.sonar.python.types.v2.TypesTestUtils.FROZENSET_TYPE;
7880
import static org.sonar.python.types.v2.TypesTestUtils.INT_TYPE;
@@ -2080,6 +2082,33 @@ void resolvedBuiltinLazyType() {
20802082
assertThat(pythonType.unwrappedType()).isEqualTo(NONE_TYPE);
20812083
}
20822084

2085+
2086+
@Test
2087+
void lazyTypeOfSuperType() {
2088+
FileInput fileInput = inferTypes("""
2089+
class MyClass(Exception):
2090+
...
2091+
MyClass
2092+
""");
2093+
ClassType myClassType = (ClassType) ((ExpressionStatement) fileInput.statements().statements().get(1)).expressions().get(0).typeV2();
2094+
assertThat(myClassType.superClasses()).extracting(TypeWrapper::type).containsExactly(EXCEPTION_TYPE);
2095+
}
2096+
2097+
2098+
@Test
2099+
void lazyTypeOfSuperType2() {
2100+
FileInput fileInput = inferTypes("""
2101+
class A: ...
2102+
A
2103+
class MyClass(A, Exception, int):
2104+
...
2105+
MyClass
2106+
""");
2107+
ClassType aType = (ClassType) ((ExpressionStatement) fileInput.statements().statements().get(1)).expressions().get(0).typeV2();
2108+
ClassType myClassType = (ClassType) ((ExpressionStatement) fileInput.statements().statements().get(3)).expressions().get(0).typeV2();
2109+
assertThat(myClassType.superClasses()).extracting(TypeWrapper::type).containsExactly(aType, EXCEPTION_TYPE, INT_TYPE);
2110+
}
2111+
20832112
@Test
20842113
void resolvedTypingLazyType() {
20852114
FileInput fileInput = inferTypes("""

python-frontend/src/test/java/org/sonar/python/types/v2/ClassTypeTest.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ void local_parent() {
9393
ClassType classC = classTypes.get(0);
9494
ClassType classB = classTypes.get(1);
9595
assertThat(classB.superClasses()).hasSize(1);
96-
assertThat(classB.superClasses()).containsExactlyInAnyOrder(classC);
96+
assertThat(classB.superClasses()).extracting(TypeWrapper::type).containsExactlyInAnyOrder(classC);
9797
}
9898

9999
@Test
@@ -109,7 +109,7 @@ void multiple_local_parents() {
109109
ClassType classA = classTypes.get(1);
110110
ClassType classB = classTypes.get(2);
111111
assertThat(classB.superClasses()).hasSize(2);
112-
assertThat(classB.superClasses()).containsExactlyInAnyOrder(classC, classA);
112+
assertThat(classB.superClasses()).extracting(TypeWrapper::type).containsExactlyInAnyOrder(classC, classA);
113113
}
114114

115115
@Test
@@ -118,7 +118,7 @@ void unknown_parent() {
118118
"class B(C): ..."
119119
);
120120
ClassType classC = classTypes.get(0);
121-
assertThat(classC.superClasses()).containsExactly(PythonType.UNKNOWN);
121+
assertThat(classC.superClasses()).extracting(TypeWrapper::type).containsExactly(PythonType.UNKNOWN);
122122
assertThat(classC.hasUnresolvedHierarchy()).isTrue();
123123
assertThat(classC.hasMember("unknown")).isEqualTo(TriBool.UNKNOWN);
124124
assertThat(classC.instancesHaveMember("unknown")).isEqualTo(TriBool.UNKNOWN);
@@ -143,7 +143,7 @@ void builtin_parent() {
143143
ClassType classB = classTypes.get(1);
144144
assertThat(classB.superClasses()).hasSize(2);
145145
assertThat(classB.hasUnresolvedHierarchy()).isFalse();
146-
var baseExceptionType = classB.superClasses().get(1);
146+
var baseExceptionType = classB.superClasses().get(1).type();
147147
assertThat(baseExceptionType)
148148
.isInstanceOf(ClassType.class)
149149
.extracting(PythonType::name)
@@ -217,7 +217,7 @@ void call_expression_argument() {
217217
"class C(foo()): ",
218218
" pass");
219219
ClassType classType = classTypes.get(0);
220-
assertThat(classType.superClasses()).containsExactly(PythonType.UNKNOWN);
220+
assertThat(classType.superClasses()).extracting(TypeWrapper::type).containsExactly(PythonType.UNKNOWN);
221221
assertThat(classType.hasUnresolvedHierarchy()).isTrue();
222222
}
223223

@@ -231,7 +231,7 @@ void parent_is_not_a_class() {
231231
" pass");
232232
ClassType classType = classTypes.get(0);
233233
assertThat(classType.superClasses()).hasSize(1);
234-
assertThat(classType.superClasses()).containsExactly(PythonType.UNKNOWN);
234+
assertThat(classType.superClasses()).extracting(TypeWrapper::type).containsExactly(PythonType.UNKNOWN);
235235
assertThat(classType.hasUnresolvedHierarchy()).isTrue();
236236
}
237237

@@ -242,7 +242,7 @@ void unpacking_expression_as_parent() {
242242
"class C(*foo): ",
243243
" pass");
244244
ClassType classType = classTypes.get(0);
245-
assertThat(classType.superClasses()).containsExactly(PythonType.UNKNOWN);
245+
assertThat(classType.superClasses()).extracting(TypeWrapper::type).containsExactly(PythonType.UNKNOWN);
246246
assertThat(classType.hasUnresolvedHierarchy()).isTrue();
247247
}
248248

@@ -347,7 +347,7 @@ void defines_attrs() {
347347
ClassType classB = classTypes.get(1);
348348
assertThat(classB.hasUnresolvedHierarchy()).isFalse();
349349
assertThat(classB.superClasses()).hasSize(1);
350-
assertThat(classB.superClasses()).extracting(PythonType::name).containsExactly("A");
350+
assertThat(classB.superClasses()).extracting(TypeWrapper::type).extracting(PythonType::name).containsExactly("A");
351351
assertThat(classB.hasMetaClass()).isFalse();
352352
assertThat(classB.instancesHaveMember("foo")).isEqualTo(TriBool.UNKNOWN);
353353
}
@@ -422,7 +422,7 @@ void class_members_with_inheritance() {
422422
" def foo(): pass").get(1);
423423

424424
assertThat(classB.members()).hasSize(1);
425-
ClassType classA = (ClassType) classB.superClasses().get(0);
425+
ClassType classA = (ClassType) classB.superClasses().get(0).type();
426426
assertThat(classA.members()).hasSize(1);
427427

428428
assertThat(classB.resolveMember("foo")).isPresent();

0 commit comments

Comments
 (0)