Skip to content

Commit 559f802

Browse files
SONARPY-1996 Implement a basic TypeChecker builder (#1853)
1 parent ccfa9e0 commit 559f802

File tree

11 files changed

+231
-5
lines changed

11 files changed

+231
-5
lines changed

python-checks/src/main/java/org/sonar/python/checks/NonCallableCalledCheck.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.sonar.plugins.python.api.tree.CallExpression;
2626
import org.sonar.plugins.python.api.tree.Expression;
2727
import org.sonar.plugins.python.api.tree.Tree;
28+
import org.sonar.python.types.v2.TypeChecker;
2829
import org.sonar.python.types.v2.PythonType;
2930
import org.sonar.python.types.v2.TriBool;
3031

@@ -39,7 +40,7 @@ public void initialize(Context context) {
3940
CallExpression callExpression = (CallExpression) ctx.syntaxNode();
4041
Expression callee = callExpression.callee();
4142
PythonType type = callee.typeV2();
42-
if (isNonCallableType(type)) {
43+
if (isNonCallableType(type, ctx.typeChecker())) {
4344
String name = nameFromExpression(callee);
4445
PreciseIssue preciseIssue = ctx.addIssue(callee, message(type, name));
4546
type.definitionLocation()
@@ -54,8 +55,8 @@ protected static String addTypeName(PythonType type) {
5455
.orElse("");
5556
}
5657

57-
public boolean isNonCallableType(PythonType type) {
58-
return type.hasMember("__call__") == TriBool.FALSE;
58+
public boolean isNonCallableType(PythonType type, TypeChecker typeChecker) {
59+
return typeChecker.typeCheckBuilder().hasMember("__call__").check(type) == TriBool.FALSE;
5960
}
6061

6162
public String message(PythonType typeV2, @Nullable String name) {

python-frontend/src/main/java/org/sonar/plugins/python/api/PythonVisitorContext.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.sonar.plugins.python.api.PythonCheck.PreciseIssue;
2929
import org.sonar.plugins.python.api.caching.CacheContext;
3030
import org.sonar.plugins.python.api.tree.FileInput;
31+
import org.sonar.python.types.v2.TypeChecker;
3132
import org.sonar.python.caching.CacheContextImpl;
3233
import org.sonar.python.semantic.ProjectLevelSymbolTable;
3334
import org.sonar.python.semantic.SymbolTableBuilder;
@@ -41,6 +42,7 @@ public class PythonVisitorContext extends PythonInputFileContext {
4142
private final FileInput rootTree;
4243
private final RecognitionException parsingException;
4344
private List<PreciseIssue> issues = new ArrayList<>();
45+
private final TypeChecker typeChecker;
4446

4547
public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable File workingDirectory, @Nullable String packageName) {
4648
super(pythonFile, workingDirectory, CacheContextImpl.dummyCache());
@@ -51,6 +53,7 @@ public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable
5153
var symbolTable = new SymbolTableBuilderV2(rootTree).build();
5254
var projectLevelTypeTable = new ProjectLevelTypeTable(ProjectLevelSymbolTable.empty(), new TypeShed(ProjectLevelSymbolTable.empty()));
5355
new TypeInferenceV2(projectLevelTypeTable, pythonFile, symbolTable).inferTypes(rootTree);
56+
this.typeChecker = new TypeChecker(projectLevelTypeTable);
5457
}
5558

5659
public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable File workingDirectory, String packageName,
@@ -64,6 +67,7 @@ public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable
6467
.build();
6568
var projectLevelTypeTable = new ProjectLevelTypeTable(projectLevelSymbolTable, new TypeShed(projectLevelSymbolTable));
6669
new TypeInferenceV2(projectLevelTypeTable, pythonFile, symbolTable).inferTypes(rootTree);
70+
this.typeChecker = new TypeChecker(projectLevelTypeTable);
6771
}
6872

6973
public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable File workingDirectory, String packageName,
@@ -76,24 +80,31 @@ public PythonVisitorContext(FileInput rootTree, PythonFile pythonFile, @Nullable
7680
.build();
7781
var projectLevelTypeTable = new ProjectLevelTypeTable(projectLevelSymbolTable, typeShed);
7882
new TypeInferenceV2(projectLevelTypeTable, pythonFile, symbolTable).inferTypes(rootTree);
83+
this.typeChecker = new TypeChecker(projectLevelTypeTable);
7984
}
8085

8186
public PythonVisitorContext(PythonFile pythonFile, RecognitionException parsingException) {
8287
super(pythonFile, null, CacheContextImpl.dummyCache());
8388
this.rootTree = null;
8489
this.parsingException = parsingException;
90+
this.typeChecker = new TypeChecker(new ProjectLevelTypeTable(ProjectLevelSymbolTable.empty()));
8591
}
8692

8793
public PythonVisitorContext(PythonFile pythonFile, RecognitionException parsingException, SonarProduct sonarProduct) {
8894
super(pythonFile, null, CacheContextImpl.dummyCache(), sonarProduct);
8995
this.rootTree = null;
9096
this.parsingException = parsingException;
97+
this.typeChecker = new TypeChecker(new ProjectLevelTypeTable(ProjectLevelSymbolTable.empty()));
9198
}
9299

93100
public FileInput rootTree() {
94101
return rootTree;
95102
}
96103

104+
public TypeChecker typeChecker() {
105+
return typeChecker;
106+
}
107+
97108
public RecognitionException parsingException() {
98109
return parsingException;
99110
}

python-frontend/src/main/java/org/sonar/plugins/python/api/SubscriptionContext.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.sonar.plugins.python.api.symbols.Symbol;
3030
import org.sonar.plugins.python.api.tree.Token;
3131
import org.sonar.plugins.python.api.tree.Tree;
32+
import org.sonar.python.types.v2.TypeChecker;
3233

3334
public interface SubscriptionContext {
3435
Tree syntaxNode();
@@ -67,4 +68,6 @@ public interface SubscriptionContext {
6768

6869
@Beta
6970
CacheContext cacheContext();
71+
72+
TypeChecker typeChecker();
7073
}

python-frontend/src/main/java/org/sonar/python/SubscriptionVisitor.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.sonar.plugins.python.api.tree.Token;
4949
import org.sonar.plugins.python.api.tree.Tree;
5050
import org.sonar.plugins.python.api.tree.Tree.Kind;
51+
import org.sonar.python.types.v2.TypeChecker;
5152
import org.sonar.python.regex.PythonAnalyzerRegexSource;
5253
import org.sonar.python.regex.PythonRegexIssueLocation;
5354
import org.sonar.python.regex.RegexContext;
@@ -182,6 +183,11 @@ public CacheContext cacheContext() {
182183
return pythonVisitorContext.cacheContext();
183184
}
184185

186+
@Override
187+
public TypeChecker typeChecker() {
188+
return pythonVisitorContext.typeChecker();
189+
}
190+
185191
public RegexParseResult regexForStringElement(StringElement stringElement, FlagSet flagSet) {
186192
return regexCache.computeIfAbsent(stringElement.hashCode() + "-" + flagSet.getMask(),
187193
s -> new RegexParser(new PythonAnalyzerRegexSource(stringElement), flagSet).parse());

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,15 @@
2525
public enum TriBool {
2626
TRUE,
2727
FALSE,
28-
UNKNOWN
28+
UNKNOWN;
29+
30+
public TriBool and(TriBool triBool) {
31+
if (this.equals(triBool)) {
32+
return this;
33+
}
34+
if (this.equals(UNKNOWN) || triBool.equals(UNKNOWN)) {
35+
return UNKNOWN;
36+
}
37+
return FALSE;
38+
}
2939
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
import java.util.ArrayList;
23+
import java.util.List;
24+
import org.sonar.python.semantic.v2.ProjectLevelTypeTable;
25+
26+
public class TypeCheckBuilder {
27+
28+
ProjectLevelTypeTable projectLevelTypeTable;
29+
List<TypePredicate> predicates = new ArrayList<>();
30+
31+
public TypeCheckBuilder(ProjectLevelTypeTable projectLevelTypeTable) {
32+
this.projectLevelTypeTable = projectLevelTypeTable;
33+
}
34+
35+
public TypeCheckBuilder hasMember(String memberName) {
36+
predicates.add(new HasMemberTypePredicate(memberName));
37+
return this;
38+
}
39+
40+
public TypeCheckBuilder instancesHaveMember(String memberName) {
41+
predicates.add(new InstancesHaveMemberTypePredicate(memberName));
42+
return this;
43+
}
44+
45+
public TriBool check(PythonType pythonType) {
46+
TriBool result = TriBool.TRUE;
47+
for (TypePredicate predicate : predicates) {
48+
TriBool partialResult = predicate.test(pythonType);
49+
result = result.and(partialResult);
50+
if (result == TriBool.UNKNOWN) {
51+
return TriBool.UNKNOWN;
52+
}
53+
}
54+
return result;
55+
}
56+
57+
interface TypePredicate {
58+
TriBool test(PythonType pythonType);
59+
}
60+
61+
static class HasMemberTypePredicate implements TypePredicate {
62+
String memberName;
63+
64+
public HasMemberTypePredicate(String memberName) {
65+
this.memberName = memberName;
66+
}
67+
68+
public TriBool test(PythonType pythonType) {
69+
return pythonType.hasMember(memberName);
70+
}
71+
}
72+
73+
static class InstancesHaveMemberTypePredicate implements TypePredicate {
74+
String memberName;
75+
76+
public InstancesHaveMemberTypePredicate(String memberName) {
77+
this.memberName = memberName;
78+
}
79+
80+
public TriBool test(PythonType pythonType) {
81+
if (pythonType instanceof ClassType classType) {
82+
return classType.instancesHaveMember(memberName);
83+
}
84+
return TriBool.FALSE;
85+
}
86+
}
87+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
import org.sonar.python.semantic.v2.ProjectLevelTypeTable;
23+
24+
public class TypeChecker {
25+
26+
private ProjectLevelTypeTable projectLevelTypeTable;
27+
28+
public TypeChecker(ProjectLevelTypeTable projectLevelTypeTable) {
29+
this.projectLevelTypeTable = projectLevelTypeTable;
30+
}
31+
32+
public TypeCheckBuilder typeCheckBuilder() {
33+
return new TypeCheckBuilder(projectLevelTypeTable);
34+
}
35+
}

python-frontend/src/test/java/org/sonar/python/SubscriptionVisitorTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2929
import org.sonar.plugins.python.api.PythonVisitorContext;
3030
import org.sonar.plugins.python.api.caching.CacheContext;
31+
import org.sonar.plugins.python.api.tree.ClassDef;
3132
import org.sonar.plugins.python.api.tree.FileInput;
3233
import org.sonar.plugins.python.api.tree.StringElement;
3334
import org.sonar.plugins.python.api.tree.Tree;
3435
import org.sonar.python.regex.RegexContext;
3536
import org.sonar.python.semantic.ProjectLevelSymbolTable;
37+
import org.sonar.python.types.v2.TriBool;
3638
import org.sonarsource.analyzer.commons.regex.RegexParseResult;
3739
import org.sonarsource.analyzer.commons.regex.ast.FlagSet;
3840

@@ -84,4 +86,21 @@ public void initialize(Context context) {
8486
};
8587
SubscriptionVisitor.analyze(Collections.singleton(check), context);
8688
}
89+
90+
@Test
91+
void typeChecker() {
92+
PythonSubscriptionCheck check = new PythonSubscriptionCheck() {
93+
@Override
94+
public void initialize(Context context) {
95+
context.registerSyntaxNodeConsumer(Tree.Kind.CLASSDEF, ctx -> {
96+
ClassDef classDef = (ClassDef) ctx.syntaxNode();
97+
assertThat(ctx.typeChecker().typeCheckBuilder().instancesHaveMember("foo").check(classDef.name().typeV2())).isEqualTo(TriBool.TRUE);
98+
});
99+
}
100+
};
101+
102+
FileInput fileInput = PythonTestUtils.parse("class A:\n def foo(self): ...");
103+
PythonVisitorContext context = new PythonVisitorContext(fileInput, PythonTestUtils.pythonFile("file"), null, null);
104+
SubscriptionVisitor.analyze(Collections.singleton(check), context);
105+
}
87106
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@
3131
import org.sonar.plugins.python.api.tree.Name;
3232
import org.sonar.plugins.python.api.tree.Tree;
3333
import org.sonar.python.PythonTestUtils;
34+
import org.sonar.python.semantic.ProjectLevelSymbolTable;
3435
import org.sonar.python.semantic.SymbolUtils;
3536
import org.sonar.python.semantic.v2.ClassTypeBuilder;
37+
import org.sonar.python.semantic.v2.ProjectLevelTypeTable;
3638
import org.sonar.python.semantic.v2.SymbolTableBuilderV2;
3739
import org.sonar.python.semantic.v2.SymbolV2;
3840
import org.sonar.python.semantic.v2.TypeInferenceV2;
@@ -46,6 +48,7 @@
4648
public class ClassTypeTest {
4749

4850
static PythonFile pythonFile = PythonTestUtils.pythonFile("");
51+
TypeChecker typeChecker = new TypeChecker(new ProjectLevelTypeTable(ProjectLevelSymbolTable.empty()));
4952

5053
@Test
5154
void no_parents() {
@@ -62,6 +65,9 @@ void no_parents() {
6265
assertThat(classType.hasMember("unknown")).isEqualTo(TriBool.UNKNOWN);
6366
assertThat(classType.instancesHaveMember("__call__")).isEqualTo(TriBool.FALSE);
6467
assertThat(classType.instancesHaveMember("unknown")).isEqualTo(TriBool.FALSE);
68+
assertThat(typeChecker.typeCheckBuilder().hasMember("__call__").check(classType)).isEqualTo(TriBool.TRUE);
69+
assertThat(typeChecker.typeCheckBuilder().hasMember("unknown").check(classType)).isEqualTo(TriBool.UNKNOWN);
70+
assertThat(typeChecker.typeCheckBuilder().instancesHaveMember("__call__").check(classType)).isEqualTo(TriBool.FALSE);
6571

6672
assertThat(classType.displayName()).contains("type");
6773
assertThat(classType.instanceDisplayName()).contains("C");

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,19 @@
3636
import org.sonar.plugins.python.api.tree.Tree;
3737
import org.sonar.plugins.python.api.tree.Tuple;
3838
import org.sonar.python.PythonTestUtils;
39-
import org.sonar.python.tree.TreeUtils;
39+
import org.sonar.python.semantic.ProjectLevelSymbolTable;
4040
import org.sonar.python.semantic.SymbolUtils;
41+
import org.sonar.python.semantic.v2.ProjectLevelTypeTable;
42+
import org.sonar.python.tree.TreeUtils;
4143

4244
import static org.assertj.core.api.Assertions.assertThat;
4345
import static org.sonar.python.types.v2.TypesTestUtils.parseAndInferTypes;
4446

4547

4648
class ObjectTypeTest {
4749

50+
TypeChecker typeChecker = new TypeChecker(new ProjectLevelTypeTable(ProjectLevelSymbolTable.empty()));
51+
4852
@Test
4953
void simpleObject() {
5054
PythonFile pythonFile = PythonTestUtils.pythonFile("");
@@ -61,6 +65,8 @@ class A: ...
6165
assertThat(objectType.displayName()).contains("A");
6266
assertThat(objectType.isCompatibleWith(classType)).isTrue();
6367
assertThat(objectType.hasMember("foo")).isEqualTo(TriBool.FALSE);
68+
assertThat(typeChecker.typeCheckBuilder().hasMember("foo").check(objectType)).isEqualTo(TriBool.FALSE);
69+
assertThat(typeChecker.typeCheckBuilder().instancesHaveMember("foo").check(objectType)).isEqualTo(TriBool.FALSE);
6470
String fileId = SymbolUtils.pathOf(pythonFile).toString();
6571
assertThat(objectType.definitionLocation()).contains(new LocationInFile(fileId, 1, 6, 1, 7));
6672
}
@@ -80,6 +86,8 @@ def foo(self): ...
8086
assertThat(objectType.displayName()).contains("A");
8187
assertThat(objectType.isCompatibleWith(classType)).isTrue();
8288
assertThat(objectType.hasMember("foo")).isEqualTo(TriBool.TRUE);
89+
assertThat(typeChecker.typeCheckBuilder().hasMember("foo").check(objectType)).isEqualTo(TriBool.TRUE);
90+
assertThat(typeChecker.typeCheckBuilder().instancesHaveMember("foo").check(objectType)).isEqualTo(TriBool.FALSE);
8391
}
8492

8593
@Test

0 commit comments

Comments
 (0)