Skip to content

Commit a8b7fc1

Browse files
SONARPY-2414 Introduce AliasDescriptor
1 parent 02afd06 commit a8b7fc1

File tree

8 files changed

+254
-2
lines changed

8 files changed

+254
-2
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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 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.index;
18+
19+
import javax.annotation.Nonnull;
20+
21+
public class AliasDescriptor implements Descriptor {
22+
23+
private final String name;
24+
private final String fullyQualifiedName;
25+
private final Descriptor originalDescriptor;
26+
27+
public AliasDescriptor(String name, String fullyQualifiedName, Descriptor originalDescriptor) {
28+
this.name = name;
29+
this.fullyQualifiedName = fullyQualifiedName;
30+
this.originalDescriptor = originalDescriptor;
31+
}
32+
33+
@Override
34+
public String name() {
35+
return this.name;
36+
}
37+
38+
@Override
39+
@Nonnull
40+
public String fullyQualifiedName() {
41+
return fullyQualifiedName;
42+
}
43+
44+
public Descriptor originalDescriptor() {
45+
return this.originalDescriptor;
46+
}
47+
48+
@Override
49+
public Kind kind() {
50+
return Kind.ALIAS;
51+
}
52+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ enum Kind {
3232
FUNCTION,
3333
CLASS,
3434
VARIABLE,
35-
AMBIGUOUS
35+
AMBIGUOUS,
36+
ALIAS
3637
}
3738
}

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,57 @@ public static Symbol symbolFromDescriptor(Descriptor descriptor, ProjectLevelSym
6767
.map(a -> DescriptorUtils.symbolFromDescriptor(a, projectLevelSymbolTable, symbolName, createdSymbolsByDescriptor, new HashMap<>()))
6868
.collect(Collectors.toSet()));
6969
return ambiguousSymbol;
70+
case ALIAS:
71+
Descriptor recreatedDescriptor = recreateDescriptorFromAlias((AliasDescriptor) descriptor);
72+
return symbolFromDescriptor(recreatedDescriptor, projectLevelSymbolTable, symbolName, createdSymbolsByDescriptor, createdSymbolsByFqn);
7073
default:
7174
throw new IllegalStateException(String.format("Error while creating a Symbol from a Descriptor: Unexpected descriptor kind: %s", descriptor.kind()));
7275
}
7376
}
7477

78+
private static Descriptor recreateDescriptorFromAlias(AliasDescriptor aliasDescriptor) {
79+
Descriptor originalDescriptor = aliasDescriptor.originalDescriptor();
80+
if (originalDescriptor instanceof FunctionDescriptor functionDescriptor) {
81+
return recreateFunctionDescriptor(aliasDescriptor, functionDescriptor);
82+
} else if (originalDescriptor instanceof ClassDescriptor classDescriptor) {
83+
return recreateClassDescriptor(aliasDescriptor, classDescriptor);
84+
}
85+
throw new IllegalStateException(String.format("Error while recreating a descriptor from an alias: Unexpected alias kind: %s", originalDescriptor.kind()));
86+
}
87+
88+
private static Descriptor recreateFunctionDescriptor(AliasDescriptor aliasDescriptor, FunctionDescriptor originalDescriptor) {
89+
// here we recreate the function descriptor from the original descriptor, only changing the name and FQN for its alias
90+
FunctionDescriptor.FunctionDescriptorBuilder builder = new FunctionDescriptor.FunctionDescriptorBuilder();
91+
return builder.withName(aliasDescriptor.name())
92+
.withFullyQualifiedName(aliasDescriptor.fullyQualifiedName())
93+
.withParameters(originalDescriptor.parameters())
94+
.withAnnotatedReturnTypeName(originalDescriptor.annotatedReturnTypeName())
95+
.withDefinitionLocation(originalDescriptor.definitionLocation())
96+
.withHasDecorators(originalDescriptor.hasDecorators())
97+
.withTypeAnnotationDescriptor(originalDescriptor.typeAnnotationDescriptor())
98+
.withDecorators(originalDescriptor.decorators())
99+
.withIsAsynchronous(originalDescriptor.isAsynchronous())
100+
.withIsInstanceMethod(originalDescriptor.isInstanceMethod())
101+
.build();
102+
}
103+
104+
private static ClassDescriptor recreateClassDescriptor(AliasDescriptor aliasDescriptor, ClassDescriptor originalDescriptor) {
105+
ClassDescriptor.ClassDescriptorBuilder builder = new ClassDescriptor.ClassDescriptorBuilder();
106+
// here we recreate the class descriptor from the original descriptor, only changing the name and FQN for its alias
107+
return builder
108+
.withName(aliasDescriptor.name())
109+
.withFullyQualifiedName(aliasDescriptor.fullyQualifiedName())
110+
.withMembers(new HashSet<>(originalDescriptor.members()))
111+
.withSuperClasses(originalDescriptor.superClasses())
112+
.withDefinitionLocation(originalDescriptor.definitionLocation())
113+
.withHasMetaClass(originalDescriptor.hasMetaClass())
114+
.withHasSuperClassWithoutDescriptor(originalDescriptor.hasSuperClassWithoutDescriptor())
115+
.withMetaclassFQN(originalDescriptor.metaclassFQN())
116+
.withHasDecorators(originalDescriptor.hasDecorators())
117+
.withSupportsGenerics(originalDescriptor.supportsGenerics())
118+
.build();
119+
}
120+
75121
private static ClassSymbolImpl createClassSymbol(Descriptor descriptor, ProjectLevelSymbolTable projectLevelSymbolTable, Map<Descriptor, Symbol> createdSymbolsByDescriptor,
76122
Map<String, Symbol> createdSymbolByFqn, String symbolName) {
77123
ClassDescriptor classDescriptor = (ClassDescriptor) descriptor;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 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.converter;
18+
19+
import org.sonar.python.index.AliasDescriptor;
20+
import org.sonar.python.index.Descriptor;
21+
import org.sonar.python.types.v2.PythonType;
22+
23+
public class AliasDescriptorToPythonTypeConverter implements DescriptorToPythonTypeConverter {
24+
@Override
25+
public PythonType convert(ConversionContext ctx, Descriptor from) {
26+
// We should try to retrieve the original type if possible, instead of recreating it
27+
return ctx.convert(((AliasDescriptor) from).originalDescriptor());
28+
}
29+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public AnyDescriptorToPythonTypeConverter(LazyTypesContext lazyTypesContext) {
3737
Descriptor.Kind.CLASS, new ClassDescriptorToPythonTypeConverter(),
3838
Descriptor.Kind.FUNCTION, new FunctionDescriptorToPythonTypeConverter(),
3939
Descriptor.Kind.VARIABLE, new VariableDescriptorToPythonTypeConverter(),
40-
Descriptor.Kind.AMBIGUOUS, new AmbiguousDescriptorToPythonTypeConverter()
40+
Descriptor.Kind.AMBIGUOUS, new AmbiguousDescriptorToPythonTypeConverter(),
41+
Descriptor.Kind.ALIAS, new AliasDescriptorToPythonTypeConverter()
4142
));
4243

4344
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import javax.annotation.CheckForNull;
2424
import javax.annotation.Nullable;
2525
import org.sonar.plugins.python.api.PythonVersionUtils;
26+
import org.sonar.python.index.AliasDescriptor;
2627
import org.sonar.python.index.Descriptor;
2728
import org.sonar.python.index.ModuleDescriptor;
2829
import org.sonar.python.types.protobuf.SymbolsProtos;
@@ -60,11 +61,13 @@ private Map<String, Descriptor> getModuleDescriptors(SymbolsProtos.ModuleSymbol
6061
.stream()
6162
.filter(d -> ProtoUtils.isValidForPythonVersion(d.getValidForList(), projectPythonVersions))
6263
.map(classConverter::convert)
64+
.map(d -> wrapInAliasIfNeeded(d, moduleSymbol.getFullyQualifiedName()))
6365
.map(Descriptor.class::cast);
6466
var functionsStream = moduleSymbol.getFunctionsList()
6567
.stream()
6668
.filter(d -> ProtoUtils.isValidForPythonVersion(d.getValidForList(), projectPythonVersions))
6769
.map(functionConverter::convert)
70+
.map(d -> wrapInAliasIfNeeded(d, moduleSymbol.getFullyQualifiedName()))
6871
.map(Descriptor.class::cast);
6972
var overloadedFunctionsStream = moduleSymbol.getOverloadedFunctionsList()
7073
.stream()
@@ -80,4 +83,20 @@ private Map<String, Descriptor> getModuleDescriptors(SymbolsProtos.ModuleSymbol
8083
return ProtoUtils.disambiguateByName(Stream.of(classesStream, functionsStream, overloadedFunctionsStream, variablesStream));
8184
}
8285

86+
private static Descriptor wrapInAliasIfNeeded(Descriptor descriptor, String moduleFullyQualifiedName) {
87+
String normalizedModuleFqn = moduleFullyQualifiedName;
88+
if (moduleFullyQualifiedName.startsWith("builtins")) {
89+
normalizedModuleFqn = moduleFullyQualifiedName.substring("builtins".length());
90+
}
91+
String descriptorFqn = descriptor.fullyQualifiedName();
92+
if (descriptorFqn == null) {
93+
return descriptor;
94+
}
95+
if (!descriptorFqn.startsWith(normalizedModuleFqn)) {
96+
String aliasFqn = normalizedModuleFqn + "." + descriptor.name();
97+
return new AliasDescriptor(descriptor.name(), aliasFqn, descriptor);
98+
}
99+
return descriptor;
100+
}
101+
83102
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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 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.index;
18+
19+
import java.util.HashMap;
20+
import java.util.Map;
21+
import org.junit.jupiter.api.Test;
22+
import org.sonar.plugins.python.api.symbols.ClassSymbol;
23+
import org.sonar.plugins.python.api.symbols.FunctionSymbol;
24+
import org.sonar.plugins.python.api.symbols.Symbol;
25+
import org.sonar.python.semantic.ProjectLevelSymbolTable;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
29+
30+
class AliasDescriptorTest {
31+
32+
@Test
33+
void aliasDescriptorOfClass() {
34+
ProjectLevelSymbolTable projectLevelSymbolTable = ProjectLevelSymbolTable.empty();
35+
Map<String, Descriptor> stringDescriptorMap = projectLevelSymbolTable.typeShedDescriptorsProvider().descriptorsForModule("fastapi.responses");
36+
AliasDescriptor aliasDescriptor = (AliasDescriptor) stringDescriptorMap.get("Response");
37+
assertThat(aliasDescriptor.name()).isEqualTo("Response");
38+
assertThat(aliasDescriptor.fullyQualifiedName()).isEqualTo("fastapi.responses.Response");
39+
assertThat(aliasDescriptor.kind()).isEqualTo(Descriptor.Kind.ALIAS);
40+
41+
Descriptor originalDescriptor = aliasDescriptor.originalDescriptor();
42+
assertThat(originalDescriptor.name()).isEqualTo("Response");
43+
assertThat(originalDescriptor.fullyQualifiedName()).isEqualTo("starlette.responses.Response");
44+
assertThat(originalDescriptor.kind()).isEqualTo(Descriptor.Kind.CLASS);
45+
46+
Symbol convertedSymbol = DescriptorUtils.symbolFromDescriptor(aliasDescriptor, projectLevelSymbolTable, null, new HashMap<>(), new HashMap<>());
47+
assertThat(convertedSymbol)
48+
.isInstanceOf(ClassSymbol.class)
49+
.hasFieldOrPropertyWithValue("name", "Response")
50+
.hasFieldOrPropertyWithValue("fullyQualifiedName", "fastapi.responses.Response");
51+
}
52+
53+
@Test
54+
void aliasDescriptorOfFunction() {
55+
ProjectLevelSymbolTable projectLevelSymbolTable = ProjectLevelSymbolTable.empty();
56+
Map<String, Descriptor> stringDescriptorMap = projectLevelSymbolTable.typeShedDescriptorsProvider().descriptorsForModule("fastapi.concurrency");
57+
AliasDescriptor aliasDescriptor = (AliasDescriptor) stringDescriptorMap.get("run_in_threadpool");
58+
assertThat(aliasDescriptor.name()).isEqualTo("run_in_threadpool");
59+
assertThat(aliasDescriptor.fullyQualifiedName()).isEqualTo("fastapi.concurrency.run_in_threadpool");
60+
assertThat(aliasDescriptor.kind()).isEqualTo(Descriptor.Kind.ALIAS);
61+
62+
Descriptor originalDescriptor = aliasDescriptor.originalDescriptor();
63+
assertThat(originalDescriptor.name()).isEqualTo("run_in_threadpool");
64+
assertThat(originalDescriptor.fullyQualifiedName()).isEqualTo("starlette.concurrency.run_in_threadpool");
65+
assertThat(originalDescriptor.kind()).isEqualTo(Descriptor.Kind.FUNCTION);
66+
67+
Symbol convertedSymbol = DescriptorUtils.symbolFromDescriptor(aliasDescriptor, projectLevelSymbolTable, null, new HashMap<>(), new HashMap<>());
68+
assertThat(convertedSymbol)
69+
.isInstanceOf(FunctionSymbol.class)
70+
.hasFieldOrPropertyWithValue("name", "run_in_threadpool")
71+
.hasFieldOrPropertyWithValue("fullyQualifiedName", "fastapi.concurrency.run_in_threadpool");
72+
}
73+
74+
@Test
75+
void aliasDescriptorOfVariableIsNotSupported() {
76+
ProjectLevelSymbolTable projectLevelSymbolTable = ProjectLevelSymbolTable.empty();
77+
AliasDescriptor aliasDescriptor = new AliasDescriptor("alias", "original", new VariableDescriptor("original", "original", null));
78+
assertThatThrownBy(() -> DescriptorUtils.symbolFromDescriptor(aliasDescriptor, projectLevelSymbolTable, null, new HashMap<>(), new HashMap<>()))
79+
.isInstanceOf(IllegalStateException.class)
80+
.hasMessage("Error while recreating a descriptor from an alias: Unexpected alias kind: VARIABLE");
81+
}
82+
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2960,6 +2960,28 @@ void resolveIncorrectLazyType2() {
29602960
assertThat(PROJECT_LEVEL_TYPE_TABLE.lazyTypesContext().getOrCreateLazyType("typing.Iterable.unknown").resolve()).isEqualTo(PythonType.UNKNOWN);
29612961
}
29622962

2963+
@Test
2964+
void convertTypeshedModuleWithAliases() {
2965+
ProjectLevelSymbolTable empty = ProjectLevelSymbolTable.empty();
2966+
ProjectLevelTypeTable projectLevelTypeTable = new ProjectLevelTypeTable(empty);
2967+
LazyTypesContext lazyTypesContext = projectLevelTypeTable.lazyTypesContext();
2968+
SymbolsModuleTypeProvider symbolsModuleTypeProvider = new SymbolsModuleTypeProvider(empty, lazyTypesContext);
2969+
ModuleType builtinModule = symbolsModuleTypeProvider.createBuiltinModule();
2970+
PythonType responses = symbolsModuleTypeProvider.convertModuleType(List.of("fastapi", "responses"), builtinModule);
2971+
assertThat(responses.resolveMember("FileResponse")).containsInstanceOf(ClassType.class);
2972+
PythonType concurrency = symbolsModuleTypeProvider.convertModuleType(List.of("fastapi", "concurrency"), builtinModule);
2973+
assertThat(concurrency.resolveMember("iterate_in_threadpool")).containsInstanceOf(FunctionType.class);
2974+
2975+
List<Symbol> fileResponseSymbols = empty.typeShedDescriptorsProvider()
2976+
.stubFilesSymbols(empty).stream().filter(s -> "fastapi.responses.FileResponse".equals(s.fullyQualifiedName())).toList();
2977+
assertThat(fileResponseSymbols).hasSize(1);
2978+
assertThat(fileResponseSymbols.get(0).kind()).isEqualTo(Symbol.Kind.CLASS);
2979+
List<Symbol> runInThreadPoolSymbols = empty.typeShedDescriptorsProvider()
2980+
.stubFilesSymbols(empty).stream().filter(s -> "fastapi.concurrency.run_in_threadpool".equals(s.fullyQualifiedName())).toList();
2981+
assertThat(runInThreadPoolSymbols).hasSize(1);
2982+
assertThat(runInThreadPoolSymbols.get(0).kind()).isEqualTo(Symbol.Kind.FUNCTION);
2983+
}
2984+
29632985
@Test
29642986
void imported_symbol_in_different_branch() {
29652987
FileInput fileInput = inferTypes("""

0 commit comments

Comments
 (0)