Skip to content

Commit abbcb92

Browse files
SONARPY-631 Add a python version parameter and raise a warning when it is not set (#945)
* SONARPY-631 Add a python version parameter and raise a warning when is not set * DeclaredType and RuntimeType for builtins should be lazily evaluated * SONARPY-631 builtins symbols should be tailored to project python version
1 parent ccc5a79 commit abbcb92

File tree

13 files changed

+454
-64
lines changed

13 files changed

+454
-64
lines changed
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-2021 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.plugins.python.api;
21+
22+
import java.util.Set;
23+
24+
import static org.sonar.plugins.python.api.PythonVersionUtils.Version;
25+
import static org.sonar.plugins.python.api.PythonVersionUtils.allVersions;
26+
27+
public class ProjectPythonVersion {
28+
29+
private ProjectPythonVersion() {
30+
}
31+
32+
private static Set<Version> currentVersions = allVersions();
33+
34+
public static Set<Version> currentVersions() {
35+
return currentVersions;
36+
}
37+
38+
public static void setCurrentVersions(Set<Version> currentVersions) {
39+
ProjectPythonVersion.currentVersions = currentVersions;
40+
}
41+
42+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2021 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.plugins.python.api;
21+
22+
import java.util.EnumSet;
23+
import java.util.HashMap;
24+
import java.util.Locale;
25+
import java.util.Map;
26+
import java.util.Set;
27+
import org.sonar.api.utils.log.Logger;
28+
import org.sonar.api.utils.log.Loggers;
29+
30+
import static org.sonar.plugins.python.api.PythonVersionUtils.Version.V_27;
31+
import static org.sonar.plugins.python.api.PythonVersionUtils.Version.V_35;
32+
import static org.sonar.plugins.python.api.PythonVersionUtils.Version.V_36;
33+
import static org.sonar.plugins.python.api.PythonVersionUtils.Version.V_37;
34+
import static org.sonar.plugins.python.api.PythonVersionUtils.Version.V_38;
35+
import static org.sonar.plugins.python.api.PythonVersionUtils.Version.V_39;
36+
37+
public class PythonVersionUtils {
38+
39+
public enum Version {
40+
V_27(2.7, "27"), V_35(3.5, "35"), V_36(3.6, "36"), V_37(3.7, "37"), V_38(3.8, "38"), V_39(3.9, "39");
41+
42+
private final double value;
43+
private final String serializedValue;
44+
45+
Version(double value, String serializedValue) {
46+
this.value = value;
47+
this.serializedValue = serializedValue;
48+
}
49+
50+
public double value() {
51+
return value;
52+
}
53+
54+
public String serializedValue() {
55+
return serializedValue;
56+
}
57+
}
58+
59+
/**
60+
* Note that versions between 3 and 3.5 are currently mapped to 3.5 because
61+
* during we don't take into account those version during typeshed symbols serialization
62+
*/
63+
private static final Map<String, Version> STRING_VERSION_MAP = new HashMap<>();
64+
static {
65+
STRING_VERSION_MAP.put("2", V_27);
66+
STRING_VERSION_MAP.put("2.7", V_27);
67+
STRING_VERSION_MAP.put("3", V_35);
68+
STRING_VERSION_MAP.put("3.0", V_35);
69+
STRING_VERSION_MAP.put("3.1", V_35);
70+
STRING_VERSION_MAP.put("3.2", V_35);
71+
STRING_VERSION_MAP.put("3.3", V_35);
72+
STRING_VERSION_MAP.put("3.4", V_35);
73+
STRING_VERSION_MAP.put("3.5", V_35);
74+
STRING_VERSION_MAP.put("3.6", V_36);
75+
STRING_VERSION_MAP.put("3.7", V_37);
76+
STRING_VERSION_MAP.put("3.8", V_38);
77+
STRING_VERSION_MAP.put("3.9", V_39);
78+
}
79+
private static final Version MIN_SUPPORTED_VERSION = V_27;
80+
private static final Version MAX_SUPPORTED_VERSION = V_39;
81+
private static final Logger LOG = Loggers.get(PythonVersionUtils.class);
82+
public static final String PYTHON_VERSION_KEY = "sonar.python.version";
83+
84+
private PythonVersionUtils() {
85+
}
86+
87+
public static Set<Version> fromString(String propertyValue) {
88+
String[] versions = propertyValue.split(",");
89+
if (versions.length == 0) {
90+
return allVersions();
91+
}
92+
Set<Version> pythonVersions = EnumSet.noneOf(Version.class);
93+
for (String versionValue : versions) {
94+
versionValue = versionValue.trim();
95+
Version version = STRING_VERSION_MAP.get(versionValue);
96+
if (version != null) {
97+
pythonVersions.add(version);
98+
} else {
99+
boolean isGuessSuccessful = guessPythonVersion(pythonVersions, versionValue);
100+
if (!isGuessSuccessful) {
101+
return allVersions();
102+
}
103+
}
104+
}
105+
return pythonVersions;
106+
}
107+
108+
public static Set<Version> allVersions() {
109+
return EnumSet.allOf(Version.class);
110+
}
111+
112+
private static boolean guessPythonVersion(Set<Version> pythonVersions, String versionValue) {
113+
try {
114+
double parsedVersion = Double.parseDouble(versionValue);
115+
if (parsedVersion < MIN_SUPPORTED_VERSION.value()) {
116+
pythonVersions.add(MIN_SUPPORTED_VERSION);
117+
} else if (parsedVersion > MAX_SUPPORTED_VERSION.value()) {
118+
pythonVersions.add(MAX_SUPPORTED_VERSION);
119+
} else {
120+
logErrorMessage(versionValue);
121+
return false;
122+
}
123+
} catch (NumberFormatException nfe) {
124+
logErrorMessage(versionValue);
125+
return false;
126+
}
127+
return true;
128+
}
129+
130+
private static void logErrorMessage(String propertyValue) {
131+
String prefix = "Error while parsing value of parameter '%s' (%s). Versions must be specified as MAJOR_VERSION.MIN.VERSION (e.g. \"3.7, 3.8\")";
132+
LOG.warn(String.format(Locale.ROOT, prefix, PYTHON_VERSION_KEY, propertyValue));
133+
}
134+
}

python-frontend/src/main/java/org/sonar/python/semantic/ClassSymbolImpl.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646

4747
import static org.sonar.python.semantic.SymbolUtils.pathOf;
4848
import static org.sonar.python.tree.TreeUtils.locationInFile;
49+
import static org.sonar.python.types.TypeShed.isValidForProjectPythonVersion;
4950
import static org.sonar.python.types.TypeShed.normalizedFqn;
5051
import static org.sonar.python.types.TypeShed.symbolsFromDescriptor;
5152

@@ -125,8 +126,12 @@ public ClassSymbolImpl(SymbolsProtos.ClassSymbol classSymbolProto) {
125126
supportsGenerics = classSymbolProto.getIsGeneric();
126127
Set<Symbol> methods = new HashSet<>();
127128
Map<String, Set<Object>> descriptorsByFqn = new HashMap<>();
128-
classSymbolProto.getMethodsList().forEach(proto -> descriptorsByFqn.computeIfAbsent(proto.getFullyQualifiedName(), d -> new HashSet<>()).add(proto));
129-
classSymbolProto.getOverloadedMethodsList().forEach(proto -> descriptorsByFqn.computeIfAbsent(proto.getFullname(), d -> new HashSet<>()).add(proto));
129+
classSymbolProto.getMethodsList().stream()
130+
.filter(d -> isValidForProjectPythonVersion(d.getValidForList()))
131+
.forEach(proto -> descriptorsByFqn.computeIfAbsent(proto.getFullyQualifiedName(), d -> new HashSet<>()).add(proto));
132+
classSymbolProto.getOverloadedMethodsList().stream()
133+
.filter(d -> isValidForProjectPythonVersion(d.getValidForList()))
134+
.forEach(proto -> descriptorsByFqn.computeIfAbsent(proto.getFullname(), d -> new HashSet<>()).add(proto));
130135
for (Map.Entry<String, Set<Object>> entry : descriptorsByFqn.entrySet()) {
131136
Set<Symbol> symbols = symbolsFromDescriptor(entry.getValue(), true);
132137
methods.add(symbols.size() > 1 ? AmbiguousSymbolImpl.create(symbols) : symbols.iterator().next());

python-frontend/src/main/java/org/sonar/python/types/DeclaredType.java

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@
3737

3838
public class DeclaredType implements InferredType {
3939

40-
private final Symbol typeClass;
40+
private Symbol typeClass;
4141
private final List<DeclaredType> typeArgs;
42-
private final Set<Symbol> alternativeTypeSymbols;
42+
private Set<Symbol> alternativeTypeSymbols;
43+
private String builtinFullyQualifiedName;
4344

4445
public DeclaredType(Symbol typeClass, List<DeclaredType> typeArgs) {
4546
this.typeClass = typeClass;
@@ -53,9 +54,9 @@ private static Set<Symbol> resolveAlternativeSymbols(Symbol typeClass, List<Decl
5354
Symbol noneType = TypeShed.typeShedClass(BuiltinTypes.NONE_TYPE);
5455
symbols.add(noneType);
5556
DeclaredType argType = typeArgs.get(0);
56-
symbols.addAll(resolveAlternativeSymbols(argType.typeClass, argType.typeArgs));
57+
symbols.addAll(resolveAlternativeSymbols(argType.getTypeClass(), argType.typeArgs));
5758
} else if ("typing.Union".equals(typeClass.fullyQualifiedName())) {
58-
symbols.addAll(typeArgs.stream().flatMap(arg -> resolveAlternativeSymbols(arg.typeClass, arg.typeArgs).stream()).collect(Collectors.toSet()));
59+
symbols.addAll(typeArgs.stream().flatMap(arg -> resolveAlternativeSymbols(arg.getTypeClass(), arg.typeArgs).stream()).collect(Collectors.toSet()));
5960
} else if ("typing.Text".equals(typeClass.fullyQualifiedName())) {
6061
symbols.add(TypeShed.typeShedClass("str"));
6162
} else {
@@ -68,6 +69,11 @@ private static Set<Symbol> resolveAlternativeSymbols(Symbol typeClass, List<Decl
6869
this(typeClass, Collections.emptyList());
6970
}
7071

72+
DeclaredType(String builtinFullyQualifiedName) {
73+
this.builtinFullyQualifiedName = builtinFullyQualifiedName;
74+
typeArgs = Collections.emptyList();
75+
}
76+
7177
@Override
7278
public boolean canHaveMember(String memberName) {
7379
return true;
@@ -78,7 +84,7 @@ public boolean declaresMember(String memberName) {
7884
if (hasUnresolvedHierarchy()) {
7985
return true;
8086
}
81-
return alternativeTypeSymbols.stream().anyMatch(symbol -> !symbol.is(CLASS) || ((ClassSymbol) symbol).canHaveMember(memberName));
87+
return alternativeTypeSymbols().stream().anyMatch(symbol -> !symbol.is(CLASS) || ((ClassSymbol) symbol).canHaveMember(memberName));
8288
}
8389

8490
@Override
@@ -139,7 +145,7 @@ public String toString() {
139145
}
140146

141147
public String typeName() {
142-
StringBuilder str = new StringBuilder(typeClass.name());
148+
StringBuilder str = new StringBuilder(getTypeClass().name());
143149
if (!typeArgs.isEmpty()) {
144150
str.append("[");
145151
str.append(typeArgs.stream().map(DeclaredType::typeName).collect(Collectors.joining(", ")));
@@ -149,10 +155,18 @@ public String typeName() {
149155
}
150156

151157
Symbol getTypeClass() {
158+
if (typeClass == null) {
159+
// the value is recomputed each time instead of storing it to avoid consistency problem when 'sonar.python.version' property is changed
160+
return TypeShed.typeShedClass(builtinFullyQualifiedName);
161+
}
152162
return typeClass;
153163
}
154164

155165
public Set<Symbol> alternativeTypeSymbols() {
166+
if (alternativeTypeSymbols == null) {
167+
// the value is recomputed each time instead of storing it to avoid consistency problem when 'sonar.python.version' property is changed
168+
return resolveAlternativeSymbols(getTypeClass(), typeArgs);
169+
}
156170
return alternativeTypeSymbols;
157171
}
158172

@@ -165,14 +179,14 @@ public boolean equals(Object o) {
165179
return false;
166180
}
167181
DeclaredType that = (DeclaredType) o;
168-
return Objects.equals(typeClass.name(), that.typeClass.name()) &&
169-
Objects.equals(typeClass.fullyQualifiedName(), that.typeClass.fullyQualifiedName()) &&
182+
return Objects.equals(getTypeClass().name(), that.getTypeClass().name()) &&
183+
Objects.equals(getTypeClass().fullyQualifiedName(), that.getTypeClass().fullyQualifiedName()) &&
170184
Objects.equals(typeArgs, that.typeArgs);
171185
}
172186

173187
@Override
174188
public int hashCode() {
175-
return Objects.hash(typeClass.name(), typeClass.fullyQualifiedName(), typeArgs);
189+
return Objects.hash(getTypeClass().name(), getTypeClass().fullyQualifiedName(), typeArgs);
176190
}
177191

178192
public static InferredType fromInferredType(InferredType inferredType) {
@@ -187,10 +201,10 @@ public static InferredType fromInferredType(InferredType inferredType) {
187201
}
188202

189203
boolean hasUnresolvedHierarchy() {
190-
if (alternativeTypeSymbols.isEmpty()) {
204+
if (alternativeTypeSymbols().isEmpty()) {
191205
return true;
192206
}
193-
for (Symbol alternativeTypeSymbol : alternativeTypeSymbols) {
207+
for (Symbol alternativeTypeSymbol : alternativeTypeSymbols()) {
194208
if (!alternativeTypeSymbol.is(CLASS) || ((ClassSymbol) alternativeTypeSymbol).hasUnresolvedTypeHierarchy()) {
195209
return true;
196210
}

python-frontend/src/main/java/org/sonar/python/types/InferredTypes.java

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,6 @@ public class InferredTypes {
8787
public static final InferredType BOOL = runtimeBuiltinType(BuiltinTypes.BOOL);
8888
public static final InferredType DECL_BOOL = declaredBuiltinType(BuiltinTypes.BOOL);
8989

90-
private static Map<String, Symbol> builtinSymbols;
91-
9290
private static final String UNICODE = "unicode";
9391
private static final String BYTES = "bytes";
9492
// https://github.com/python/mypy/blob/e97377c454a1d5c019e9c56871d5f229db6b47b2/mypy/semanal_classprop.py#L16-L46
@@ -108,20 +106,16 @@ public class InferredTypes {
108106
private InferredTypes() {
109107
}
110108

111-
public static boolean isInitialized() {
112-
return builtinSymbols != null;
113-
}
114-
115109
public static InferredType anyType() {
116110
return AnyType.ANY;
117111
}
118112

119113
static InferredType runtimeBuiltinType(String fullyQualifiedName) {
120-
return new RuntimeType(TypeShed.typeShedClass(fullyQualifiedName));
114+
return new RuntimeType(fullyQualifiedName);
121115
}
122116

123117
private static InferredType declaredBuiltinType(String fullyQualifiedName) {
124-
return new DeclaredType(TypeShed.typeShedClass(fullyQualifiedName));
118+
return new DeclaredType(fullyQualifiedName);
125119
}
126120

127121
public static InferredType runtimeType(@Nullable Symbol typeClass) {
@@ -134,10 +128,6 @@ public static InferredType runtimeType(@Nullable Symbol typeClass) {
134128
return anyType();
135129
}
136130

137-
static void setBuiltinSymbols(Map<String, Symbol> builtinSymbols) {
138-
InferredTypes.builtinSymbols = Collections.unmodifiableMap(builtinSymbols);
139-
}
140-
141131
public static InferredType or(InferredType t1, InferredType t2) {
142132
return UnionType.or(t1, t2);
143133
}
@@ -147,7 +137,7 @@ public static InferredType union(Stream<InferredType> types) {
147137
}
148138

149139
public static InferredType fromTypeAnnotation(TypeAnnotation typeAnnotation) {
150-
Map<String, Symbol> builtins = InferredTypes.builtinSymbols != null ? InferredTypes.builtinSymbols : Collections.emptyMap();
140+
Map<String, Symbol> builtins = TypeShed.builtinSymbols();
151141
DeclaredType declaredType = declaredTypeFromTypeAnnotation(typeAnnotation.expression(), builtins);
152142
if (declaredType == null) {
153143
return InferredTypes.anyType();
@@ -156,7 +146,7 @@ public static InferredType fromTypeAnnotation(TypeAnnotation typeAnnotation) {
156146
}
157147

158148
public static InferredType fromTypeshedTypeAnnotation(TypeAnnotation typeAnnotation) {
159-
Map<String, Symbol> builtins = InferredTypes.builtinSymbols != null ? InferredTypes.builtinSymbols : Collections.emptyMap();
149+
Map<String, Symbol> builtins = TypeShed.builtinSymbols();
160150
return runtimeTypefromTypeAnnotation(typeAnnotation.expression(), builtins);
161151
}
162152

0 commit comments

Comments
 (0)