Skip to content

Commit

Permalink
SONARPY-1561: Infer types of variables used as decorators. (#1668)
Browse files Browse the repository at this point in the history
  • Loading branch information
joke1196 authored Dec 1, 2023
1 parent 1833127 commit f572992
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Set, FrozenSet, Union, Coroutine
from typing import Set, FrozenSet, Union, Coroutine, Callable
import asyncio

def empty_union(x: Union['A', 'B']):
Expand Down Expand Up @@ -37,3 +37,15 @@ async def bar(func: Coroutine):
await asyncio.gather(
func()
)


def decorators(decorator: Callable, non_callable: str):

@non_callable() # Noncompliant
def foo():
...

@decorator()
def bar():
...

20 changes: 20 additions & 0 deletions python-checks/src/test/resources/checks/nonCallableCalled.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ class A(Base): ...
a = A()
a() # OK


def decorators():
x = 42
@x() # Noncompliant
def foo():
...

#######################################
# Valid case: Calling a callable object
#######################################
Expand All @@ -97,6 +104,19 @@ def call_function():
func()
max()

#############################
# Valid case: Call a decorator
#############################

def decorators():
class Dec:
def __call__(self, *args):
...

@Dec("foo")
def foo():
...

#############################
# Valid case: Call a method
#############################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,7 @@ private PythonCfgBlock build(Statement statement, PythonCfgBlock currentBlock) {
case WITH_STMT:
return buildWithStatement((WithStatement) statement, currentBlock);
case CLASSDEF:
ClassDef classDef = (ClassDef) statement;
PythonCfgBlock block = build(classDef.body().statements(), currentBlock);
block.addElement(classDef.name()); // represents binding to class name
return block;
return buildClassDefStatement((ClassDef) statement, currentBlock);
case RETURN_STMT:
return buildReturnStatement((ReturnStatement) statement, currentBlock);
case RAISE_STMT:
Expand All @@ -186,13 +183,28 @@ private PythonCfgBlock build(Statement statement, PythonCfgBlock currentBlock) {
return buildBreakStatement((BreakStatement) statement, currentBlock);
case MATCH_STMT:
return buildMatchStatement((MatchStatement) statement, currentBlock);
case FUNCDEF:
return buildFuncDefStatement((FunctionDef) statement, currentBlock);
default:
currentBlock.addElement(statement);
}

return currentBlock;
}

private PythonCfgBlock buildClassDefStatement(ClassDef classDef, PythonCfgBlock currentBlock) {
PythonCfgBlock block = build(classDef.body().statements(), currentBlock);
block.addElement(classDef.name()); // represents binding to class name
classDef.decorators().stream().forEach(currentBlock::addElement);
return block;
}

private static PythonCfgBlock buildFuncDefStatement(FunctionDef functionDef, PythonCfgBlock currentBlock) {
currentBlock.addElement(functionDef);
functionDef.decorators().stream().forEach(currentBlock::addElement);
return currentBlock;
}

private PythonCfgBlock buildMatchStatement(MatchStatement statement, PythonCfgBlock successor) {
List<CaseBlock> caseBlocks = statement.caseBlocks();
PythonCfgBlock matchingBlock = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package org.sonar.plugins.python.api.cfg;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
Expand Down Expand Up @@ -706,6 +707,34 @@ void if_stmt_test_predecessors() {
"after_if(succ = [END], pred = [before, if_body])");
}

@Test
void decorators() {
ControlFlowGraph cfg = cfg(
"class Dec:",
" def a():",
" ...",
"dec = Dec()",
"@dec.a()",
"def foo():",
" ..."
);
List<Tree> elements = cfg.start().elements();
assertThat(elements).hasSize(5);
assertThat(elements).extracting(Tree::getKind).containsExactly(Kind.NAME, Kind.FUNCDEF, Kind.ASSIGNMENT_STMT, Kind.DECORATOR, Kind.FUNCDEF);

cfg = cfg(
"class Dec:",
" def __call__(self):",
" ...",
"@Dec",
"class OtherClass:",
" ..."
);
elements = cfg.start().elements();
assertThat(elements).hasSize(5);
assertThat(elements).extracting(Tree::getKind).containsExactly(Kind.NAME, Kind.FUNCDEF, Kind.DECORATOR, Kind.NAME, Kind.EXPRESSION_STMT);
}

@Test
void parameters() {
FileInput fileInput = PythonTestUtils.parse("def f(p1, p2): pass");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,31 @@
import org.sonar.plugins.python.api.symbols.ClassSymbol;
import org.sonar.plugins.python.api.tree.AssignmentStatement;
import org.sonar.plugins.python.api.tree.CallExpression;
import org.sonar.plugins.python.api.tree.Decorator;
import org.sonar.plugins.python.api.tree.Expression;
import org.sonar.plugins.python.api.tree.ExpressionStatement;
import org.sonar.plugins.python.api.tree.FileInput;
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.ReturnStatement;
import org.sonar.plugins.python.api.tree.Tree;
import org.sonar.plugins.python.api.types.BuiltinTypes;
import org.sonar.plugins.python.api.types.InferredType;
import org.sonar.python.PythonTestUtils;
import org.sonar.python.semantic.SymbolImpl;
import org.sonar.python.semantic.SymbolTableBuilder;

import static org.assertj.core.api.Assertions.assertThat;
import static org.sonar.python.PythonTestUtils.getLastDescendant;
import static org.sonar.python.PythonTestUtils.lastExpression;
import static org.sonar.python.PythonTestUtils.lastExpressionInFunction;
import static org.sonar.python.PythonTestUtils.lastStatement;import static org.sonar.python.PythonTestUtils.parse;
import static org.sonar.python.PythonTestUtils.lastStatement;
import static org.sonar.python.PythonTestUtils.parse;
import static org.sonar.python.PythonTestUtils.pythonFile;
import static org.sonar.python.types.InferredTypes.BOOL;
import static org.sonar.python.types.InferredTypes.COMPLEX;
import static org.sonar.python.types.InferredTypes.DECL_INT;
import static org.sonar.python.types.InferredTypes.DECL_LIST;
import static org.sonar.python.types.InferredTypes.DECL_STR;
import static org.sonar.python.types.InferredTypes.DICT;
import static org.sonar.python.types.InferredTypes.FLOAT;
Expand Down Expand Up @@ -759,4 +764,36 @@ void user_defined_attributes_reassigned() {
).type()).isEqualTo(DECL_INT);
}

@Test
void decorators() {
Decorator decorator = lastDecorator(
"class A:",
" def dec_method():",
" ...",
"my_dec = A()",
"@my_dec.dec_method()",
"def a_function():",
" ...");
CallExpression ce = (CallExpression) decorator.expression();
QualifiedExpression qe = (QualifiedExpression) ce.callee();
assertThat(typeName(qe.qualifier().type())).isEqualTo("A");
assertThat(qe.name().symbol().fullyQualifiedName()).isEqualTo("some_package.some_module.A.dec_method");
assertThat(ce.calleeSymbol().fullyQualifiedName()).isEqualTo("some_package.some_module.A.dec_method");

decorator = lastDecorator(
"class A:",
" def __call__():",
" ...",
"@A",
"class OtherClass:",
" ...");
Name name = (Name) decorator.expression();
assertThat(name.type()).isEqualTo(anyType());
assertThat(name.symbol().fullyQualifiedName()).isEqualTo("some_package.some_module.A");
}

private static Decorator lastDecorator(String... code) {
FileInput fileInput = parse(new SymbolTableBuilder("some_package", pythonFile("some_module")), code);
return PythonTestUtils.getLastDescendant(fileInput, t -> t.is(Tree.Kind.DECORATOR));
}
}

0 comments on commit f572992

Please sign in to comment.