Skip to content

Commit

Permalink
SONARPY-2382 fix unary propagation problem (#2198)
Browse files Browse the repository at this point in the history
  • Loading branch information
Seppli11 authored Dec 3, 2024
1 parent 393af24 commit 1adc874
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@

def param_with_type_hint(a: int):
a() # OK
a() # OK

def call_unknown_value(unknown_value):
(~unknown_value)(X) # Ok
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package org.sonar.python.semantic.v2.types;

import org.sonar.plugins.python.api.tree.BaseTreeVisitor;
import org.sonar.plugins.python.api.tree.Token;
import org.sonar.plugins.python.api.tree.UnaryExpression;
import org.sonar.plugins.python.api.types.BuiltinTypes;
import org.sonar.python.semantic.v2.TypeTable;
Expand Down Expand Up @@ -52,22 +51,32 @@ public TrivialTypePropagationVisitor(TypeTable typeTable) {
public void visitUnaryExpression(UnaryExpression unaryExpr) {
super.visitUnaryExpression(unaryExpr);

Token operator = unaryExpr.operator();
PythonType exprType = switch (operator.value()) {
case "~" -> intType;
case "not" -> boolType;
case "+", "-" -> getTypeWhenUnaryPlusMinus(unaryExpr);
default -> PythonType.UNKNOWN;
};

PythonType exprType = calculateUnaryExprType(unaryExpr);
if (unaryExpr instanceof UnaryExpressionImpl unaryExprImpl) {
unaryExprImpl.typeV2(toObjectType(exprType));
}
}

private PythonType getTypeWhenUnaryPlusMinus(UnaryExpression unaryExpr) {
var innerExprType = unaryExpr.expression().typeV2();
return TypeUtils.map(innerExprType, this::mapUnaryPlusMinusType);
private PythonType calculateUnaryExprType(UnaryExpression unaryExpr) {
String operator = unaryExpr.operator().value();
return TypeUtils.map(unaryExpr.expression().typeV2(), type -> mapUnaryExprType(operator, type));
}

private PythonType mapUnaryExprType(String operator, PythonType type) {
return switch (operator) {
case "~" -> mapInvertExprType(type);
// not cannot be overloaded and always returns a boolean
case "not" -> boolType;
case "+", "-" -> mapUnaryPlusMinusType(type);
default -> PythonType.UNKNOWN;
};
}

private PythonType mapInvertExprType(PythonType type) {
if(isIntTypeCheck.check(type) == TriBool.TRUE || isBooleanTypeCheck.check(type) == TriBool.TRUE) {
return intType;
}
return PythonType.UNKNOWN;
}

private PythonType mapUnaryPlusMinusType(PythonType type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.sonar.python.tree.TokenImpl;
import org.sonar.python.tree.UnaryExpressionImpl;
import org.sonar.python.types.v2.ObjectType;
import org.sonar.python.types.v2.PythonType;
import org.sonar.python.types.v2.TypesTestUtils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.python.PythonTestUtils.lastExpression;
import static org.sonar.python.PythonTestUtils.pythonFile;
import static org.sonar.python.types.v2.TypesTestUtils.PROJECT_LEVEL_TYPE_TABLE;
Expand Down Expand Up @@ -54,15 +58,13 @@ static Stream<Arguments> testSources() {
Arguments.of("+(True)", TypesTestUtils.INT_TYPE),

Arguments.of("~1", TypesTestUtils.INT_TYPE),
Arguments.of("~1.0", TypesTestUtils.INT_TYPE),
Arguments.of("~(3+2j)", TypesTestUtils.INT_TYPE),
Arguments.of("~(1j)", TypesTestUtils.INT_TYPE),
Arguments.of("~(True)", TypesTestUtils.INT_TYPE),

Arguments.of("not 1", TypesTestUtils.BOOL_TYPE),
Arguments.of("not 1.0", TypesTestUtils.BOOL_TYPE),
Arguments.of("not (2j)", TypesTestUtils.BOOL_TYPE),
Arguments.of("not (True)", TypesTestUtils.BOOL_TYPE)
Arguments.of("not (True)", TypesTestUtils.BOOL_TYPE),
Arguments.of("not x", TypesTestUtils.BOOL_TYPE)
);
}

Expand All @@ -77,12 +79,32 @@ void test(String code, PythonType expectedType) {
assertThat(objectType.type()).isEqualTo(expectedType));
}

static Stream<Arguments> testUnknownReturnSources() {
return Stream.of(
Arguments.of("~x"),
Arguments.of("~1.0"),
Arguments.of("~(1j)"),
Arguments.of("~(3+2j)"),
Arguments.of("-x"),
Arguments.of("+x")
);
}

@ParameterizedTest
@MethodSource("testUnknownReturnSources")
void testUnknownReturn(String code) {
var expr = lastExpression(code);
expr.accept(trivialTypeInferenceVisitor);
expr.accept(trivialTypePropagationVisitor);
assertThat(expr.typeV2()).isEqualTo(PythonType.UNKNOWN);
}

static Stream<Arguments> customNumberClassTestSource() {
return Stream.of(
Arguments.of("+(MyNum())", PythonType.UNKNOWN),
Arguments.of("-(MyNum())", PythonType.UNKNOWN),
Arguments.of("not (MyNum())", new ObjectType(TypesTestUtils.BOOL_TYPE)),
Arguments.of("~(MyNum())", new ObjectType(TypesTestUtils.INT_TYPE))
Arguments.of("~(MyNum())", PythonType.UNKNOWN)
);
}

Expand All @@ -105,4 +127,16 @@ void testNotOfCustomClass() {
assertThat(expr.typeV2()).isInstanceOfSatisfying(ObjectType.class, objectType ->
assertThat(objectType.type()).isEqualTo(TypesTestUtils.BOOL_TYPE));
}

@Test
void testUnknownOperator() {
var operator = mock(TokenImpl.class);
when(operator.value()).thenReturn("invalid_operator");
UnaryExpressionImpl expr = new UnaryExpressionImpl(operator, lastExpression("1"));
expr.typeV2(TypesTestUtils.INT_TYPE);

expr.accept(trivialTypePropagationVisitor);
assertThat(expr.typeV2()).isEqualTo(PythonType.UNKNOWN);
}

}

0 comments on commit 1adc874

Please sign in to comment.