Skip to content

Commit

Permalink
SONARPY-1817 Ensure member access types are resolved correctly (#1802)
Browse files Browse the repository at this point in the history
  • Loading branch information
1 parent 63303ea commit d15a0ec
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 30 deletions.
11 changes: 11 additions & 0 deletions python-checks/src/test/resources/checks/nonCallableCalled.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,14 @@ def my_rec(x):
my_rec(True)
finally:
my_rec = None

def call_non_callable_property():
e = OSError()
e.errno() # FN

class MyClass:
x = 42

def foo():
mc = MyClass()
mc.x()
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import org.sonar.plugins.python.api.tree.AnnotatedAssignment;
import org.sonar.plugins.python.api.tree.AssignmentStatement;
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
import org.sonar.plugins.python.api.tree.CompoundAssignmentStatement;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.FunctionDef;
Expand All @@ -35,9 +33,7 @@
import org.sonar.python.cfg.fixpoint.ForwardAnalysis;
import org.sonar.python.cfg.fixpoint.ProgramState;
import org.sonar.python.semantic.v2.SymbolV2;
import org.sonar.python.tree.NameImpl;
import org.sonar.python.types.v2.PythonType;
import org.sonar.python.types.v2.UnionType;

public class FlowSensitiveTypeInference extends ForwardAnalysis {
private final Set<SymbolV2> trackedVars;
Expand Down Expand Up @@ -99,31 +95,7 @@ public void updateProgramState(Tree element, ProgramState programState) {
}

private static void updateTree(Tree tree, TypeInferenceProgramState state) {
tree.accept(new BaseTreeVisitor() {
@Override
public void visitName(Name name) {
Optional.ofNullable(name.symbolV2()).ifPresent(symbol -> {
Set<PythonType> pythonTypes = state.getTypes(symbol);
if (!pythonTypes.isEmpty()) {
((NameImpl) name).typeV2(union(pythonTypes.stream()));
}
});
super.visitName(name);
}

@Override
public void visitFunctionDef(FunctionDef pyFunctionDefTree) {
// skip inner functions
}
});
}

public static PythonType or(PythonType t1, PythonType t2) {
return UnionType.or(t1, t2);
}

public static PythonType union(Stream<PythonType> types) {
return types.reduce(FlowSensitiveTypeInference::or).orElse(PythonType.UNKNOWN);
tree.accept(new ProgramStateTypeInferenceVisitor(state));
}

private void handleAssignment(Statement assignmentStatement, TypeInferenceProgramState programState) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* SonarQube Python Plugin
* Copyright (C) 2011-2024 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.python.semantic.v2.types;

import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.FunctionDef;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.QualifiedExpression;
import org.sonar.python.tree.NameImpl;
import org.sonar.python.types.v2.PythonType;
import org.sonar.python.types.v2.UnionType;

/**
* Used in FlowSensitiveTypeInference to update name types based on program state
*/
public class ProgramStateTypeInferenceVisitor extends BaseTreeVisitor {
private final TypeInferenceProgramState state;

public ProgramStateTypeInferenceVisitor(TypeInferenceProgramState state) {
this.state = state;
}

@Override
public void visitName(Name name) {
Optional.ofNullable(name.symbolV2()).ifPresent(symbol -> {
Set<PythonType> pythonTypes = state.getTypes(symbol);
if (!pythonTypes.isEmpty()) {
((NameImpl) name).typeV2(union(pythonTypes.stream()));
}
});
super.visitName(name);
}

@Override
public void visitFunctionDef(FunctionDef pyFunctionDefTree) {
// skip inner functions
}

@Override
public void visitQualifiedExpression(QualifiedExpression qualifiedExpression) {
scan(qualifiedExpression.qualifier());
if (qualifiedExpression.name() instanceof NameImpl name) {
Optional.of(qualifiedExpression.qualifier())
.map(Expression::typeV2)
.flatMap(t -> t.resolveMember(name.name()))
.ifPresent(name::typeV2);
}
}

private static PythonType or(PythonType t1, PythonType t2) {
return UnionType.or(t1, t2);
}

private static PythonType union(Stream<PythonType> types) {
return types.reduce(ProgramStateTypeInferenceVisitor::or).orElse(PythonType.UNKNOWN);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.sonar.plugins.python.api.tree.ImportFrom;
import org.sonar.plugins.python.api.tree.ImportName;
import org.sonar.plugins.python.api.tree.Name;
import org.sonar.plugins.python.api.tree.QualifiedExpression;
import org.sonar.plugins.python.api.tree.RegularArgument;
import org.sonar.plugins.python.api.tree.Statement;
import org.sonar.plugins.python.api.tree.StatementList;
Expand All @@ -67,6 +68,7 @@
class TypeInferenceV2Test {

static PythonFile pythonFile = PythonTestUtils.pythonFile("");
private static ProjectLevelTypeTable projectLevelTypeTable;

@Test
void testTypeshedImports() {
Expand Down Expand Up @@ -991,6 +993,201 @@ void try_except_list_attributes() {

}

@Test
void inferTypeForQualifiedExpression() {
var root = inferTypes("""
class A:
def foo():
...
def f():
a = A()
a.foo()
""");

var fooMethodType = TreeUtils.firstChild(root.statements().statements().get(0), FunctionDef.class::isInstance)
.map(FunctionDef.class::cast)
.map(FunctionDef::name)
.map(Expression::typeV2)
.get();

var qualifiedExpression = TreeUtils.firstChild(root.statements().statements().get(1), QualifiedExpression.class::isInstance)
.map(QualifiedExpression.class::cast)
.get();

Assertions.assertThat(qualifiedExpression)
.isNotNull()
.extracting(QualifiedExpression::typeV2)
.isNotNull();

var qualifiedExpressionType = qualifiedExpression.typeV2();
Assertions.assertThat(qualifiedExpressionType)
.isSameAs(fooMethodType)
.isInstanceOf(FunctionType.class)
.extracting(PythonType::name)
.isEqualTo("foo");
}

@Test
void inferTypeForVariableAssignedToQualifiedExpression() {
var root = inferTypes("""
class A:
def foo():
...
def f():
a = A()
b = a.foo
b()
""");

var fooMethodType = TreeUtils.firstChild(root.statements().statements().get(0), FunctionDef.class::isInstance)
.map(FunctionDef.class::cast)
.map(FunctionDef::name)
.map(Expression::typeV2)
.get();

var qualifiedExpression = TreeUtils.firstChild(root.statements().statements().get(1), QualifiedExpression.class::isInstance)
.map(QualifiedExpression.class::cast)
.get();

Assertions.assertThat(qualifiedExpression)
.isNotNull()
.extracting(QualifiedExpression::typeV2)
.isNotNull();

var qualifiedExpressionType = qualifiedExpression.typeV2();
Assertions.assertThat(qualifiedExpressionType)
.isSameAs(fooMethodType)
.isInstanceOf(FunctionType.class)
.extracting(PythonType::name)
.isEqualTo("foo");

var bType = TreeUtils.firstChild(root.statements().statements().get(1), FunctionDef.class::isInstance)
.map(FunctionDef.class::cast)
.map(FunctionDef::body)
.map(StatementList::statements)
.map(s -> s.get(2))
.flatMap(s -> TreeUtils.firstChild(s, Name.class::isInstance))
.map(Name.class::cast)
.map(Expression::typeV2)
.get();

Assertions.assertThat(qualifiedExpressionType).isSameAs(bType);
}

@Test
@Disabled("Member overrides are not supported")
void inferTypeForOverridenMemberQualifiedExpression() {
var root = inferTypes("""
class A:
def foo():
...
def f():
a = A()
a.foo = 42
a.foo()
""");


var fBodyStatements = TreeUtils.firstChild(root.statements().statements().get(1), FunctionDef.class::isInstance)
.map(FunctionDef.class::cast)
.map(FunctionDef::body)
.map(StatementList::statements)
.get();


var qualifiedExpressionType = TreeUtils.firstChild(fBodyStatements.get(2), QualifiedExpression.class::isInstance)
.map(QualifiedExpression.class::cast)
.map(QualifiedExpression::typeV2)
.map(PythonType::unwrappedType)
.get();

var builtinsIntType = projectLevelTypeTable.getModule()
.resolveMember("int")
.get();

Assertions.assertThat(qualifiedExpressionType)
.isSameAs(builtinsIntType);
}


@Test
void inferBuiltinsTypeForQualifiedExpression() {
var root = inferTypes("""
a = [42]
a.append(1)
""");

var qualifiedExpression = TreeUtils.firstChild(root.statements().statements().get(1), QualifiedExpression.class::isInstance)
.map(QualifiedExpression.class::cast)
.get();

Assertions.assertThat(qualifiedExpression)
.isNotNull()
.extracting(QualifiedExpression::typeV2)
.isNotNull();

var builtinsListType = projectLevelTypeTable.getModule()
.resolveMember("list")
.get();

var builtinsAppendType = builtinsListType.resolveMember("append").get();

var qualifierType = qualifiedExpression.qualifier().typeV2().unwrappedType();
Assertions.assertThat(qualifierType).isSameAs(builtinsListType);

var qualifiedExpressionType = qualifiedExpression.typeV2();
Assertions.assertThat(qualifiedExpressionType)
.isSameAs(builtinsAppendType)
.isInstanceOf(FunctionType.class)
.extracting(PythonType::name)
.isEqualTo("append");
}

@Test
void inferUnknownTypeNestedQualifiedExpression() {
var root = inferTypes("""
def f():
a = foo()
a.b.c
""");

var qualifiedExpression = TreeUtils.firstChild(root.statements().statements().get(0), QualifiedExpression.class::isInstance)
.map(QualifiedExpression.class::cast)
.get();

Assertions.assertThat(qualifiedExpression)
.isNotNull()
.extracting(QualifiedExpression::typeV2)
.isNotNull();
}

@Test
@Disabled("Attribute types resolving")
void inferBuiltinsAttributeTypeForQualifiedExpression() {
var root = inferTypes("""
def f():
e = OSError()
e.errno
""");

var qualifiedExpression = TreeUtils.firstChild(root.statements().statements().get(0), QualifiedExpression.class::isInstance)
.map(QualifiedExpression.class::cast)
.get();

Assertions.assertThat(qualifiedExpression)
.isNotNull()
.extracting(QualifiedExpression::typeV2)
.isNotNull();

var qualifiedExpressionType = qualifiedExpression.typeV2();
Assertions.assertThat(qualifiedExpressionType)
.isInstanceOf(ObjectType.class)
.extracting(ObjectType.class::cast)
.extracting(ObjectType::type)
.extracting(PythonType::name)
.isEqualTo("int");
}

private static FileInput inferTypes(String lines) {
return inferTypes(lines, new HashMap<>());
}
Expand All @@ -1000,7 +1197,8 @@ private static FileInput inferTypes(String lines, Map<String, Set<Symbol>> globa

var symbolTable = new SymbolTableBuilderV2(root)
.build();
new TypeInferenceV2(new ProjectLevelTypeTable(ProjectLevelSymbolTable.from(globalSymbols)), pythonFile, symbolTable).inferTypes(root);
projectLevelTypeTable = new ProjectLevelTypeTable(ProjectLevelSymbolTable.from(globalSymbols));
new TypeInferenceV2(projectLevelTypeTable, pythonFile, symbolTable).inferTypes(root);
return root;
}

Expand Down

0 comments on commit d15a0ec

Please sign in to comment.