Skip to content

Commit a10f50e

Browse files
guillaume-dequennesonartech
authored andcommitted
SONARPY-4789 Model generic information for S6543
GitOrigin-RevId: 211a014b4566b2adff61a4e73b4209b10317ad15
1 parent db47014 commit a10f50e

4,058 files changed

Lines changed: 1087 additions & 372 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎python-checks/src/main/java/org/sonar/python/checks/GenericTypeWithoutArgumentCheck.java‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.sonar.plugins.python.api.tree.SubscriptionExpression;
3030
import org.sonar.plugins.python.api.tree.Tree;
3131
import org.sonar.plugins.python.api.tree.TypeAnnotation;
32+
import org.sonar.plugins.python.api.types.v2.GenericInfo;
3233
import org.sonar.python.semantic.ClassSymbolImpl;
3334
import org.sonar.python.tree.NameImpl;
3435

@@ -58,7 +59,7 @@ private static void checkForGenericTypeWithoutArgument(SubscriptionContext subsc
5859
return;
5960
}
6061

61-
if (typeSupportsGenericsOrIsACollection(expression) || qualifiedExpressionIsACollection(expression)) {
62+
if (shouldReportMissingTypeArguments(expression) || qualifiedExpressionIsACollection(expression)) {
6263
subscriptionContext.addIssue(expression, MESSAGE);
6364
}
6465
}
@@ -68,13 +69,18 @@ private static void checkNestedTypes(SubscriptionContext subscriptionContext, Su
6869
expressions.forEach(nestedTypeExpression -> checkForGenericTypeWithoutArgument(subscriptionContext, nestedTypeExpression));
6970
}
7071

71-
private static boolean typeSupportsGenericsOrIsACollection(Expression expression) {
72+
private static boolean shouldReportMissingTypeArguments(Expression expression) {
7273
if (expression.is(Tree.Kind.NAME)) {
7374
NameImpl name = (NameImpl) expression;
74-
if (name.symbol() instanceof ClassSymbolImpl maybeSymbol) {
75-
return Optional.of(maybeSymbol)
76-
.map(ClassSymbolImpl::supportsGenerics)
77-
.orElse(false);
75+
if (name.symbol() instanceof ClassSymbolImpl classSymbol) {
76+
if (classSymbol.genericInfo() instanceof GenericInfo.Generic(var bareTypeArguments)) {
77+
// Preserve the rule's existing reporting policy when default information is unavailable.
78+
return switch (bareTypeArguments) {
79+
case NO_ANY -> false;
80+
case CONTAINS_ANY, UNKNOWN -> true;
81+
};
82+
}
83+
return false;
7884
} else {
7985
return isACollection(name.symbol());
8086
}

‎python-checks/src/test/java/org/sonar/python/checks/GenericTypeWithoutArgumentCheckTest.java‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,26 @@
1717
package org.sonar.python.checks;
1818

1919
import java.util.List;
20+
import java.util.stream.Stream;
2021
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.Arguments;
24+
import org.junit.jupiter.params.provider.MethodSource;
25+
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
26+
import org.sonar.plugins.python.api.tree.Name;
27+
import org.sonar.plugins.python.api.types.v2.GenericInfo.BareTypeArgumentStatus;
28+
import org.sonar.plugins.python.api.types.v2.GenericInfo;
29+
import org.sonar.python.SubscriptionVisitor;
30+
import org.sonar.python.TestPythonVisitorRunner;
31+
import org.sonar.python.caching.CacheContextImpl;
2132
import org.sonar.python.checks.utils.PythonCheckVerifier;
33+
import org.sonar.python.index.GenericInfoProtobuf;
34+
import org.sonar.python.semantic.ClassSymbolImpl;
35+
import org.sonar.python.semantic.ProjectLevelSymbolTable;
36+
import org.sonar.python.tree.NameImpl;
37+
import org.sonar.python.types.protobuf.SymbolsProtos;
38+
39+
import static org.assertj.core.api.Assertions.assertThat;
2240

2341
class GenericTypeWithoutArgumentCheckTest {
2442

@@ -27,6 +45,48 @@ void test() {
2745
PythonCheckVerifier.verify("src/test/resources/checks/genericTypeWithoutArgument.py", new GenericTypeWithoutArgumentCheck());
2846
}
2947

48+
@ParameterizedTest
49+
@MethodSource("genericStates")
50+
void generic_defaults_in_annotations(GenericInfo genericInfo, int expectedIssues) {
51+
var file = new TestPythonVisitorRunner.MockPythonFile("", "defaults.py", """
52+
value: Generic
53+
nested: list[Generic]
54+
def fn(arg: Generic) -> Generic:
55+
...
56+
""");
57+
var context = TestPythonVisitorRunner.createContext(file, null, "", ProjectLevelSymbolTable.empty(), CacheContextImpl.dummyCache());
58+
var descriptor = SymbolsProtos.ClassSymbol.newBuilder().setName("Generic").setFullyQualifiedName("defaults.Generic")
59+
.setIsGeneric(true).setGenericInfo(GenericInfoProtobuf.toProtobuf(genericInfo)).build();
60+
var symbol = new ClassSymbolImpl(descriptor, "defaults");
61+
// Inject a serialized class independently of the bundled Typeshed revision.
62+
context.rootTree().accept(new BaseTreeVisitor() {
63+
@Override
64+
public void visitName(Name name) {
65+
if ("Generic".equals(name.name())) {
66+
((NameImpl) name).setSymbol(symbol);
67+
}
68+
}
69+
});
70+
SubscriptionVisitor.analyze(List.of(new GenericTypeWithoutArgumentCheck()), context);
71+
assertThat(context.getIssues()).hasSize(expectedIssues);
72+
}
73+
74+
static Stream<Arguments> genericStates() {
75+
return Stream.of(
76+
Arguments.of(GenericInfo.UNKNOWN, 0),
77+
Arguments.of(GenericInfo.NON_GENERIC, 0),
78+
Arguments.of(new GenericInfo.Generic(BareTypeArgumentStatus.UNKNOWN), 4),
79+
Arguments.of(new GenericInfo.Generic(BareTypeArgumentStatus.CONTAINS_ANY), 4),
80+
Arguments.of(new GenericInfo.Generic(BareTypeArgumentStatus.NO_ANY), 0));
81+
}
82+
83+
@Test
84+
void concrete_defaults_survive_project_reexports() {
85+
PythonCheckVerifier.verify(List.of(
86+
"src/test/resources/checks/genericDefaultsExported.py",
87+
"src/test/resources/checks/genericDefaultsImporting.py"), new GenericTypeWithoutArgumentCheck());
88+
}
89+
3090
@Test
3191
void test_cross_file() {
3292
PythonCheckVerifier.verify(
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from anyio.abc import TaskStatus
2+
from typing import Generic, TypeVar
3+
4+
T = TypeVar("T")
5+
6+
class Required(Generic[T]):
7+
pass
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from anyio.abc import TaskStatus as DirectStatus
2+
from genericDefaultsExported import TaskStatus as ReexportedStatus, Required
3+
4+
direct: DirectStatus
5+
reexported: ReexportedStatus
6+
nested: list[ReexportedStatus]
7+
8+
def status(arg: ReexportedStatus) -> ReexportedStatus:
9+
...
10+
11+
required: Required # Noncompliant
12+
explicit: Required[int]

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public final class ClassType implements PythonType {
4444
private final List<PythonType> metaClasses;
4545
private final boolean hasDecorators;
4646
private final List<DecoratorInfo> decorators;
47-
private final boolean isGeneric;
47+
private final GenericInfo genericInfo;
4848
private final LocationInFile locationInFile;
4949

5050
public ClassType(
@@ -71,6 +71,22 @@ public ClassType(
7171
boolean isGeneric,
7272
@Nullable LocationInFile locationInFile,
7373
List<DecoratorInfo> decorators) {
74+
this(name, fullyQualifiedName, members, attributes, superClasses, metaClasses, hasDecorators,
75+
GenericInfo.fromLegacy(isGeneric), locationInFile, decorators);
76+
}
77+
78+
public ClassType(
79+
String name,
80+
String fullyQualifiedName,
81+
Set<Member> members,
82+
List<PythonType> attributes,
83+
List<TypeWrapper> superClasses,
84+
List<PythonType> metaClasses,
85+
boolean hasDecorators,
86+
GenericInfo genericInfo,
87+
@Nullable LocationInFile locationInFile,
88+
List<DecoratorInfo> decorators) {
89+
this.genericInfo = genericInfo;
7490
this.decorators = List.copyOf(decorators);
7591
this.name = name;
7692
this.fullyQualifiedName = fullyQualifiedName;
@@ -79,7 +95,6 @@ public ClassType(
7995
this.superClasses = superClasses;
8096
this.metaClasses = metaClasses;
8197
this.hasDecorators = hasDecorators || !decorators.isEmpty();
82-
this.isGeneric = isGeneric;
8398
this.locationInFile = locationInFile;
8499
}
85100

@@ -364,7 +379,11 @@ public boolean hasDecorators() {
364379
return hasDecorators;
365380
}
366381

382+
public GenericInfo genericInfo() {
383+
return genericInfo;
384+
}
385+
367386
public boolean isGeneric() {
368-
return isGeneric;
387+
return genericInfo.isGeneric();
369388
}
370389
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* You can redistribute and/or modify this program under the terms of
7+
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
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.plugins.python.api.types.v2;
18+
19+
import org.sonar.api.Beta;
20+
21+
/**
22+
* Declaration-level generic information, independent of supplied type arguments.
23+
* Unknown genericity is distinct from a known generic whose omitted arguments are unknown.
24+
*/
25+
@Beta
26+
public sealed interface GenericInfo {
27+
GenericInfo UNKNOWN = new Unknown();
28+
GenericInfo NON_GENERIC = new NonGeneric();
29+
30+
/** Compatibility query: false does not establish that a class is non-generic. */
31+
default boolean isGeneric() {
32+
return this instanceof Generic;
33+
}
34+
35+
/** Legacy flags establish genericity only when true; they carry no default information. */
36+
static GenericInfo fromLegacy(boolean isGeneric) {
37+
return isGeneric ? new Generic(BareTypeArgumentStatus.UNKNOWN) : UNKNOWN;
38+
}
39+
40+
record Unknown() implements GenericInfo {
41+
}
42+
43+
record NonGeneric() implements GenericInfo {
44+
}
45+
46+
/** Information about effective type arguments when no explicit arguments are supplied. */
47+
record Generic(BareTypeArgumentStatus bareTypeArguments) implements GenericInfo {
48+
}
49+
50+
enum BareTypeArgumentStatus {
51+
/** Unsupported constructs, unresolved dependencies, or unavailable metadata. */
52+
UNKNOWN,
53+
/** At least one effective argument contains Any, including an omitted required parameter. */
54+
CONTAINS_ANY,
55+
/** Every effective argument has been established to be free of Any. */
56+
NO_ANY
57+
}
58+
}

‎python-frontend/src/main/java/org/sonar/python/index/ClassDescriptor.java‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import javax.annotation.Nonnull;
2626
import javax.annotation.Nullable;
2727
import org.sonar.plugins.python.api.LocationInFile;
28+
import org.sonar.plugins.python.api.types.v2.GenericInfo;
2829

2930
public class ClassDescriptor implements Descriptor {
3031

@@ -40,12 +41,13 @@ public class ClassDescriptor implements Descriptor {
4041
private final boolean hasMetaClass;
4142
private final String metaclassFQN;
4243
private final List<Descriptor> metaClasses;
43-
private final boolean supportsGenerics;
44+
private final GenericInfo genericInfo;
4445
private final boolean isSelf;
4546

4647
private ClassDescriptor(String name, String fullyQualifiedName, Collection<String> superClasses, Set<Descriptor> members,
4748
List<Descriptor> attributes, boolean hasDecorators, @Nullable LocationInFile definitionLocation, boolean hasSuperClassWithoutDescriptor,
48-
boolean hasMetaClass, @Nullable String metaclassFQN, List<Descriptor> metaClasses, boolean supportsGenerics, boolean isSelf, List<DecoratorDescriptor> decorators) {
49+
boolean hasMetaClass, @Nullable String metaclassFQN, List<Descriptor> metaClasses, GenericInfo genericInfo, boolean isSelf,
50+
List<DecoratorDescriptor> decorators) {
4951

5052
this.name = name;
5153
this.fullyQualifiedName = fullyQualifiedName;
@@ -59,7 +61,7 @@ private ClassDescriptor(String name, String fullyQualifiedName, Collection<Strin
5961
this.hasMetaClass = hasMetaClass;
6062
this.metaclassFQN = metaclassFQN;
6163
this.metaClasses = metaClasses;
62-
this.supportsGenerics = supportsGenerics;
64+
this.genericInfo = genericInfo;
6365
this.isSelf = isSelf;
6466
}
6567

@@ -123,7 +125,11 @@ public List<Descriptor> metaClasses() {
123125
}
124126

125127
public boolean supportsGenerics() {
126-
return supportsGenerics;
128+
return genericInfo.isGeneric();
129+
}
130+
131+
public GenericInfo genericInfo() {
132+
return genericInfo;
127133
}
128134

129135
public boolean isSelf() {
@@ -144,7 +150,7 @@ public static class ClassDescriptorBuilder {
144150
private boolean hasMetaClass = false;
145151
private String metaclassFQN = null;
146152
private List<Descriptor> metaClasses = new ArrayList<>();
147-
private boolean supportsGenerics = false;
153+
private GenericInfo genericInfo = GenericInfo.UNKNOWN;
148154
private boolean isSelf = false;
149155

150156
public ClassDescriptorBuilder withName(String name) {
@@ -207,8 +213,8 @@ public ClassDescriptorBuilder withMetaClasses(List<Descriptor> metaClasses) {
207213
return this;
208214
}
209215

210-
public ClassDescriptorBuilder withSupportsGenerics(boolean supportsGenerics) {
211-
this.supportsGenerics = supportsGenerics;
216+
public ClassDescriptorBuilder withGenericInfo(GenericInfo genericInfo) {
217+
this.genericInfo = genericInfo;
212218
return this;
213219
}
214220

@@ -219,7 +225,7 @@ public ClassDescriptorBuilder withIsSelf(boolean isSelf) {
219225

220226
public ClassDescriptor build() {
221227
return new ClassDescriptor(name, fullyQualifiedName, superClasses, members, attributes, hasDecorators, definitionLocation,
222-
hasSuperClassWithoutDescriptor, hasMetaClass, metaclassFQN, metaClasses, supportsGenerics, isSelf, decorators);
228+
hasSuperClassWithoutDescriptor, hasMetaClass, metaclassFQN, metaClasses, genericInfo, isSelf, decorators);
223229
}
224230
}
225231
}

‎python-frontend/src/main/java/org/sonar/python/index/DescriptorUtils.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
import java.util.Map;
2323
import java.util.Set;
2424
import java.util.stream.Collectors;
25-
import javax.annotation.Nullable;
2625
import javax.annotation.CheckForNull;
26+
import javax.annotation.Nullable;
2727
import org.sonar.plugins.python.api.symbols.Symbol;
2828
import org.sonar.plugins.python.api.types.InferredType;
2929
import org.sonar.python.index.TypeAnnotationDescriptor.TypeKind;
@@ -118,7 +118,7 @@ private static ClassDescriptor recreateClassDescriptor(AliasDescriptor aliasDesc
118118
.withMetaclassFQN(originalDescriptor.metaclassFQN())
119119
.withHasDecorators(originalDescriptor.hasDecorators())
120120
.withDecorators(originalDescriptor.decorators())
121-
.withSupportsGenerics(originalDescriptor.supportsGenerics())
121+
.withGenericInfo(originalDescriptor.genericInfo())
122122
.build();
123123
}
124124

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ private static DescriptorsProtos.ClassDescriptor toProtobuf(ClassDescriptor clas
8787
.setHasSuperClassWithoutDescriptor(classDescriptor.hasSuperClassWithoutDescriptor())
8888
.setHasMetaClass(classDescriptor.hasMetaClass())
8989
.setMetaClasses(toProtobufDescriptorList(classDescriptor.metaClasses()))
90-
.setSupportsGenerics(classDescriptor.supportsGenerics());
90+
.setSupportsGenerics(classDescriptor.supportsGenerics())
91+
.setGenericInfo(GenericInfoProtobuf.toProtobuf(classDescriptor.genericInfo()));
9192
LocationInFile definitionLocation = classDescriptor.definitionLocation();
9293
if (definitionLocation != null) {
9394
builder.setDefinitionLocation(toProtobuf(definitionLocation));
@@ -246,7 +247,7 @@ private static ClassDescriptor fromProtobuf(DescriptorsProtos.ClassDescriptor cl
246247
.withHasMetaClass(classDescriptorProto.getHasMetaClass())
247248
.withMetaclassFQN(metaclassFQN)
248249
.withMetaClasses(metaClasses)
249-
.withSupportsGenerics(classDescriptorProto.getSupportsGenerics())
250+
.withGenericInfo(GenericInfoProtobuf.fromProtobuf(classDescriptorProto))
250251
.build();
251252
}
252253

0 commit comments

Comments
 (0)