diff --git a/python-checks/src/main/java/org/sonar/python/checks/ArgumentTypeCheck.java b/python-checks/src/main/java/org/sonar/python/checks/ArgumentTypeCheck.java new file mode 100644 index 0000000000..08151559a2 --- /dev/null +++ b/python-checks/src/main/java/org/sonar/python/checks/ArgumentTypeCheck.java @@ -0,0 +1,201 @@ +/* + * SonarQube Python Plugin + * Copyright (C) 2011-2020 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.checks; + +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; +import org.sonar.check.Rule; +import org.sonar.plugins.python.api.LocationInFile; +import org.sonar.plugins.python.api.PythonSubscriptionCheck; +import org.sonar.plugins.python.api.SubscriptionContext; +import org.sonar.plugins.python.api.symbols.FunctionSymbol; +import org.sonar.plugins.python.api.symbols.Symbol; +import org.sonar.plugins.python.api.tree.Argument; +import org.sonar.plugins.python.api.tree.CallExpression; +import org.sonar.plugins.python.api.tree.Name; +import org.sonar.plugins.python.api.tree.RegularArgument; +import org.sonar.plugins.python.api.tree.Tree; +import org.sonar.plugins.python.api.types.BuiltinTypes; +import org.sonar.plugins.python.api.types.InferredType; + +@Rule(key = "S5655") +public class ArgumentTypeCheck extends PythonSubscriptionCheck { + + @Override + public void initialize(Context context) { + context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, ctx -> { + CallExpression callExpression = (CallExpression) ctx.syntaxNode(); + Symbol calleeSymbol = callExpression.calleeSymbol(); + if (calleeSymbol == null) { + return; + } + if (!calleeSymbol.is(Symbol.Kind.FUNCTION)) { + // We might want to support ambiguous symbols for which every definition is a function + return; + } + FunctionSymbol functionSymbol = (FunctionSymbol) calleeSymbol; + if (functionSymbol.hasVariadicParameter()) { + return; + } + checkFunctionCall(ctx, callExpression, functionSymbol); + }); + } + + private static void checkFunctionCall(SubscriptionContext ctx, CallExpression callExpression, FunctionSymbol functionSymbol) { + boolean isKeyword = false; + int firstParameterOffset = firstParameterOffset(functionSymbol); + if (firstParameterOffset < 0) { + return; + } + for (int i = 0; i < callExpression.arguments().size(); i++) { + Argument argument = callExpression.arguments().get(i); + int parameterIndex = i + firstParameterOffset; + if (parameterIndex >= functionSymbol.parameters().size()) { + // S930 will raise the issue + return; + } + if (argument.is(Tree.Kind.REGULAR_ARGUMENT)) { + RegularArgument regularArgument = (RegularArgument) argument; + isKeyword |= regularArgument.keywordArgument() != null; + boolean shouldReport = isKeyword ? shouldReportKeywordArgument(regularArgument, functionSymbol) + : shouldReportPositionalArgument(regularArgument, functionSymbol, parameterIndex); + if (shouldReport) { + reportIssue(ctx, functionSymbol, regularArgument); + } + } + } + } + + private static boolean shouldReportPositionalArgument(RegularArgument regularArgument, FunctionSymbol functionSymbol, int index) { + FunctionSymbol.Parameter functionParameter = functionSymbol.parameters().get(index); + InferredType argumentType = regularArgument.expression().type(); + InferredType parameterType = functionParameter.declaredType(); + if (parameterType.canOnlyBe("object")) { + // Avoid FPs as every Python 3 class implicitly inherits from object + return false; + } + return isIncompatibleTypes(argumentType, parameterType); + } + + private static boolean shouldReportKeywordArgument(RegularArgument regularArgument, FunctionSymbol functionSymbol) { + Name keywordArgument = regularArgument.keywordArgument(); + InferredType argumentType = regularArgument.expression().type(); + if (keywordArgument == null) { + // Syntax error + return false; + } + String keywordName = keywordArgument.name(); + Optional correspondingParameter = functionSymbol.parameters().stream().filter(p -> keywordName.equals(p.name())).findFirst(); + return correspondingParameter + .map(c -> { + InferredType parameterType = c.declaredType(); + return (isIncompatibleTypes(argumentType, parameterType)); + }) + // If not present: S930 will raise the issue + .orElse(false); + } + + private static void reportIssue(SubscriptionContext ctx, FunctionSymbol functionSymbol, RegularArgument regularArgument) { + PreciseIssue issue = ctx.addIssue(regularArgument, String.format("Change this argument; Function \"%s\" expects a different type", functionSymbol.name())); + LocationInFile locationInFile = functionSymbol.definitionLocation(); + if (locationInFile != null) { + issue.secondary(locationInFile, "Function definition"); + } + } + + private static boolean isIncompatibleTypes(InferredType argumentType, InferredType parameterType) { + return isNotDuckTypeCompatible(argumentType, parameterType) + || (!argumentType.isCompatibleWith(parameterType) && !couldBeDuckTypeCompatible(argumentType, parameterType)); + } + + private static boolean isNotDuckTypeCompatible(InferredType argumentType, InferredType parameterType) { + // Avoid FNs if builtins have incomplete type hierarchy when we are certain of their type + String firstBuiltin = matchBuiltinCategory(argumentType::canOnlyBe); + String secondBuiltin = matchBuiltinCategory(parameterType::canOnlyBe); + return firstBuiltin != null && secondBuiltin != null && !firstBuiltin.equals(secondBuiltin); + } + + private static boolean couldBeDuckTypeCompatible(InferredType firstType, InferredType secondType) { + // Here we'll return true if we cannot exclude possible duck typing because of unresolved type hierarchies or typing aliases + String firstPossibleBuiltin = matchBuiltinCategory(firstType::canBeOrExtend); + String secondPossibleBuiltin = matchBuiltinCategory(secondType::canBeOrExtend); + return firstPossibleBuiltin != null && firstPossibleBuiltin.equals(secondPossibleBuiltin); + } + + public static String matchBuiltinCategory(Predicate predicate) { + if (predicate.test(BuiltinTypes.STR)) { + return BuiltinTypes.STR; + } + if (predicate.test(BuiltinTypes.INT) + || predicate.test(BuiltinTypes.FLOAT) + || predicate.test(BuiltinTypes.COMPLEX) + || predicate.test(BuiltinTypes.BOOL)) { + return "number"; + } + if (predicate.test(BuiltinTypes.LIST)) { + return BuiltinTypes.LIST; + } + if (predicate.test(BuiltinTypes.SET)) { + return BuiltinTypes.SET; + } + if (predicate.test(BuiltinTypes.DICT)) { + return BuiltinTypes.DICT; + } + if (predicate.test(BuiltinTypes.TUPLE)) { + return BuiltinTypes.TUPLE; + } + return null; + } + + /* + He were return the offset between parameter position and argument position: + 0 if there is no implicit first parameter (self, cls, etc...) + 1 if there is an implicit first parameter + -1 if unknown (intent is not clear from function definition) + */ + public static int firstParameterOffset(FunctionSymbol functionSymbol) { + List parameters = functionSymbol.parameters(); + if (parameters.isEmpty()) { + return 0; + } + String firstParamName = parameters.get(0).name(); + if (firstParamName == null) { + // Should never happen + return -1; + } + if (functionSymbol.isInstanceMethod() && firstParamName.equals("self")) { + // If first param is not "self", we can't rule out the possibility of a static method missing the "@staticmethod" annotation + return 1; + } + List decoratorNames = functionSymbol.decorators(); + if (decoratorNames.size() > 1) { + // We want to avoid FP if there are many decorators + return -1; + } + if (decoratorNames.size() == 1 && decoratorNames.get(0).endsWith("classmethod")) { + return 1; + } + if (!functionSymbol.isInstanceMethod()) { + return 0; + } + return -1; + } +} diff --git a/python-checks/src/main/java/org/sonar/python/checks/CheckList.java b/python-checks/src/main/java/org/sonar/python/checks/CheckList.java index 1c996b315b..325e0b9ff4 100644 --- a/python-checks/src/main/java/org/sonar/python/checks/CheckList.java +++ b/python-checks/src/main/java/org/sonar/python/checks/CheckList.java @@ -58,6 +58,7 @@ public static Iterable getChecks() { AfterJumpStatementCheck.class, AllBranchesAreIdenticalCheck.class, ArgumentNumberCheck.class, + ArgumentTypeCheck.class, BackslashInStringCheck.class, BackticksUsageCheck.class, BareRaiseInFinallyCheck.class, diff --git a/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S5655.html b/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S5655.html new file mode 100644 index 0000000000..6f2dae2f9c --- /dev/null +++ b/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S5655.html @@ -0,0 +1,16 @@ +

Python does not check the type of arguments provided to functions. However builtin functions and methods expect a specific type for each parameter. +Providing an argument of the wrong type will make your program fail.

+

This rule raises an issue when a builtin function is called with an argument of the wrong type.

+

Noncompliant Code Example

+
+round("42.3")  # Noncompliant
+
+

Compliant Solution

+
+round(42.3)
+
+

See

+ + diff --git a/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S5655.json b/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S5655.json new file mode 100644 index 0000000000..c7c5351d29 --- /dev/null +++ b/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S5655.json @@ -0,0 +1,12 @@ +{ + "title": "Arguments given to functions should be of an expected type", + "type": "BUG", + "status": "ready", + "tags": [ + + ], + "defaultSeverity": "Blocker", + "ruleSpecification": "RSPEC-5655", + "sqKey": "S5655", + "scope": "All" +} diff --git a/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json b/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json index 0e47896d19..001d4f8a23 100644 --- a/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json +++ b/python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json @@ -86,6 +86,7 @@ "S5527", "S5603", "S5632", + "S5655", "S5685", "S5704", "S5706", diff --git a/python-checks/src/test/java/org/sonar/python/checks/ArgumentTypeCheckTest.java b/python-checks/src/test/java/org/sonar/python/checks/ArgumentTypeCheckTest.java new file mode 100644 index 0000000000..116cc8d6ef --- /dev/null +++ b/python-checks/src/test/java/org/sonar/python/checks/ArgumentTypeCheckTest.java @@ -0,0 +1,31 @@ +/* + * SonarQube Python Plugin + * Copyright (C) 2011-2020 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.checks; + +import org.junit.Test; +import org.sonar.python.checks.utils.PythonCheckVerifier; + +public class ArgumentTypeCheckTest { + + @Test + public void test() { + PythonCheckVerifier.verify("src/test/resources/checks/argumentType.py", new ArgumentTypeCheck()); + } +} diff --git a/python-checks/src/test/resources/checks/argumentType.py b/python-checks/src/test/resources/checks/argumentType.py new file mode 100644 index 0000000000..40be524439 --- /dev/null +++ b/python-checks/src/test/resources/checks/argumentType.py @@ -0,0 +1,151 @@ +from math import acos +import datetime +import time +import select +import genericpath +import _heapq +import imaplib +from typing import Dict, Tuple, Set + +class ExpectedClass(): + a = 42 + def expected_method(): ... +class ExpectedSubClass(ExpectedClass): ... +class DuckTypeCompatibleClass: + a = 42 + def expected_method(): ... + def some_other_method(): ... +class UnexpectedClass(): a = 42 + +def functions_defined_locally(): + def function_with_int(a: int): ... + function_with_int("154") # Noncompliant + function_with_int(154) # OK + + def function_with_custom_type_arg(smth: ExpectedClass, a: int): ... + unexpected = UnexpectedClass() + expected = ExpectedClass() + my_int = 42 + ducktyped = DuckTypeCompatibleClass() + function_with_custom_type_arg(unexpected) # Noncompliant + function_with_custom_type_arg(a = my_int, smth = expected) + function_with_custom_type_arg(a = my_int, smth = unexpected) # Noncompliant + function_with_custom_type_arg(expected, my_int, 42) # S930 will handle this + function_with_custom_type_arg(ducktyped, my_int) # OK, a class is ducktype compatible with another if it has the same members and methods + + def function_with_keyword_only(smth: ExpectedClass, *, other: ExpectedClass): ... + function_with_keyword_only(expected, other = expected) + function_with_keyword_only(expected, other = unexpected) # Noncompliant {{Change this argument; Function "function_with_keyword_only" expects a different type}} +# ^^^^^^^^^^^^^^^^^^ + + def function_with_positional_only(smth: ExpectedClass, /, other: ExpectedClass): ... + function_with_positional_only(expected, expected) + function_with_positional_only(expected, other = expected) + function_with_positional_only(expected, other = unexpected) # Noncompliant + function_with_positional_only(expected, unexpected) # Noncompliant + + class SomeClass(): + def method_with_positional_and_keyword_only(self, smth: ExpectedClass, /, other: int, *, then: ExpectedClass): ... + my_SomeClass = SomeClass() + my_SomeClass.method_with_positional_and_keyword_only(expected, 42, then = ExpectedSubClass()) + my_SomeClass.method_with_positional_and_keyword_only(expected, my_int, then = ExpectedSubClass()) + my_SomeClass.method_with_positional_and_keyword_only(expected, 42, then = UnexpectedClass()) # Noncompliant + my_SomeClass.method_with_positional_and_keyword_only(expected, 42, then = unexpected) # Noncompliant + +def stdlib_functions(): + A = UnexpectedClass() + acos(A) # Noncompliant + B = datetime.tzinfo() + B.tzname(42) # Noncompliant {{Change this argument; Function "tzname" expects a different type}} +# ^^ + select.select([],[],[], 0) + time.sleep(1) # OK + x = time.gmtime(int(time.time())) + x = time.gmtime(secs = int(time.time())) + time.sleep(True) # OK, converted to 1 + time.sleep(1j) # FN, considered duck type compatible + genericpath.isfile("some/path") + genericpath.isfile(42) # FN: Text is ambiguous + my_list = [1,2,3] + _heapq.heapify(42) # Noncompliant {{Change this argument; Function "heapify" expects a different type}} +# ^^ + _heapq.heapify(my_list) + imap4 = imaplib.IMAP4() + imap4.setannotation(42) # FN, we do not handle variadic parameters + imap4.setannotation("some string") # OK + str_tuple = "string", "another string" + imap4.setannotation(str_tuple) # OK + +def builtin_functions(): + round(42.3) + round("42.3") # FN, ambiguous symbol: no parameters defined yet | missing type hierarchy + unexpected = UnexpectedClass() + number = 42 + number.__add__("27") # Noncompliant + number.__add__(unexpected) # Noncompliant + number.__add__(x = unexpected) # Noncompliant {{Change this argument; Function "__add__" expects a different type}} +# ^^^^^^^^^^^^^^ + float.fromhex(42) # Noncompliant + eval(42) + "Some string literal".format(1, 2) + exit(1) + repr(A) + arr = [] + len(arr) # OK, duck type compatibility + +def type_aliases(): + def with_set(a : Set[int]): ... + def with_dict(a : Dict[int, int]): ... + def with_tuple(a : Tuple[int]): ... + + with_set({42}) # OK + with_set({1 : 42}) # Noncompliant + with_set((42, 43)) # Noncompliant + with_set(a = 42) # Noncompliant + + with_dict({42}) # Noncompliant + with_dict({1 : 42}) # OK + with_dict((42, 43)) # Noncompliant + with_dict(42) # Noncompliant + + with_tuple({42}) # Noncompliant + with_tuple({1 : 42}) # Noncompliant + with_tuple((42, 43)) # OK + with_tuple(42) # Noncompliant + +def edge_cases(): + ambiguous = 42 + def ambiguous(a: str): ... + ambiguous(42) # OK + def func(a: int, b: int): ... + func(b = 42, 42) # not a valid syntax + func(*unpack) # OK + unknown_call(1,2,3) + def func_no_parameters(): ... + func_no_parameters() + def func_only_keywords(*, arg: str): ... + func_only_keywords(arg = 42) # Noncompliant + + class SomeClass(): + def my_method(self, x: int): ... + @staticmethod + def static_method(y: str): ... + @classmethod + def class_method(cls, y: str): ... + def ambiguous_static_method(y: str): ... + @unknowndecorator + def method_with_unknowndecorator(y: str): ... + @decorator1 + @decorator2 + def method_with_multiple_decorators(y: str): ... + A = SomeClass() + A.my_method("42") # Noncompliant + A.my_method(42) + SomeClass.ambiguous_static_method("some string") + SomeClass.ambiguous_static_method(42) # OK, ambiguous as not explicitly a static method + A.static_method(42) # Noncompliant + SomeClass.static_method(42) # Noncompliant + A.class_method(42) # Noncompliant + SomeClass.class_method(42) # Noncompliant + A.method_with_unknowndecorator(42) # OK, unknown decorator + A.method_with_multiple_decorators(42) # OK, multiple decorators diff --git a/python-frontend/src/main/java/org/sonar/plugins/python/api/symbols/FunctionSymbol.java b/python-frontend/src/main/java/org/sonar/plugins/python/api/symbols/FunctionSymbol.java index 6fca848787..9db8231607 100644 --- a/python-frontend/src/main/java/org/sonar/plugins/python/api/symbols/FunctionSymbol.java +++ b/python-frontend/src/main/java/org/sonar/plugins/python/api/symbols/FunctionSymbol.java @@ -22,6 +22,7 @@ import java.util.List; import javax.annotation.CheckForNull; import org.sonar.plugins.python.api.LocationInFile; +import org.sonar.plugins.python.api.types.InferredType; public interface FunctionSymbol extends Symbol { List parameters(); @@ -38,6 +39,8 @@ public interface FunctionSymbol extends Symbol { boolean isInstanceMethod(); + List decorators(); + boolean hasDecorators(); @CheckForNull @@ -46,6 +49,7 @@ public interface FunctionSymbol extends Symbol { interface Parameter { @CheckForNull String name(); + InferredType declaredType(); boolean hasDefaultValue(); boolean isKeywordOnly(); boolean isPositionalOnly(); diff --git a/python-frontend/src/main/java/org/sonar/plugins/python/api/types/InferredType.java b/python-frontend/src/main/java/org/sonar/plugins/python/api/types/InferredType.java index dfa95b52bc..8cd70875ac 100644 --- a/python-frontend/src/main/java/org/sonar/plugins/python/api/types/InferredType.java +++ b/python-frontend/src/main/java/org/sonar/plugins/python/api/types/InferredType.java @@ -41,4 +41,7 @@ public interface InferredType { @Beta boolean canBeOrExtend(String typeName); + @Beta + boolean isCompatibleWith(InferredType other); + } diff --git a/python-frontend/src/main/java/org/sonar/python/semantic/FunctionSymbolImpl.java b/python-frontend/src/main/java/org/sonar/python/semantic/FunctionSymbolImpl.java index ca873bb227..92fbd6845a 100644 --- a/python-frontend/src/main/java/org/sonar/python/semantic/FunctionSymbolImpl.java +++ b/python-frontend/src/main/java/org/sonar/python/semantic/FunctionSymbolImpl.java @@ -22,6 +22,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.plugins.python.api.LocationInFile; @@ -29,11 +30,13 @@ import org.sonar.plugins.python.api.symbols.FunctionSymbol; import org.sonar.plugins.python.api.symbols.Symbol; import org.sonar.plugins.python.api.tree.AnyParameter; +import org.sonar.plugins.python.api.tree.Decorator; import org.sonar.plugins.python.api.tree.FunctionDef; import org.sonar.plugins.python.api.tree.Name; import org.sonar.plugins.python.api.tree.ParameterList; import org.sonar.plugins.python.api.tree.Token; import org.sonar.plugins.python.api.tree.Tree; +import org.sonar.plugins.python.api.tree.TypeAnnotation; import org.sonar.plugins.python.api.types.InferredType; import org.sonar.python.TokenLocation; import org.sonar.python.types.InferredTypes; @@ -42,6 +45,7 @@ public class FunctionSymbolImpl extends SymbolImpl implements FunctionSymbol { private final List parameters = new ArrayList<>(); + private final List decorators; private final LocationInFile functionDefinitionLocation; private boolean hasVariadicParameter = false; private final boolean isInstanceMethod; @@ -49,29 +53,34 @@ public class FunctionSymbolImpl extends SymbolImpl implements FunctionSymbol { private InferredType declaredReturnType = InferredTypes.anyType(); private boolean isStub = false; private Symbol owner; + private static final String CLASS_METHOD_DECORATOR = "classmethod"; + private static final String STATIC_METHOD_DECORATOR = "staticmethod"; FunctionSymbolImpl(FunctionDef functionDef, @Nullable String fullyQualifiedName, PythonFile pythonFile) { super(functionDef.name().name(), fullyQualifiedName); setKind(Kind.FUNCTION); isInstanceMethod = isInstanceMethod(functionDef); hasDecorators = !functionDef.decorators().isEmpty(); + decorators = decorators(functionDef); String fileId = null; if (!SymbolUtils.isTypeShedFile(pythonFile)) { Path path = pathOf(pythonFile); fileId = path != null ? path.toString() : pythonFile.toString(); } - ParameterList parametersList = functionDef.parameters(); - if (parametersList != null) { - createParameterNames(parametersList.all(), fileId); - } functionDefinitionLocation = locationInFile(functionDef.name(), fileId); } + public void setParametersWithType(ParameterList parametersList) { + this.parameters.clear(); + createParameterNames(parametersList.all(), functionDefinitionLocation == null ? null : functionDefinitionLocation.fileId()); + } + FunctionSymbolImpl(String name, FunctionSymbol functionSymbol) { super(name, functionSymbol.fullyQualifiedName()); setKind(Kind.FUNCTION); isInstanceMethod = functionSymbol.isInstanceMethod(); hasDecorators = functionSymbol.hasDecorators(); + decorators = functionSymbol.decorators(); hasVariadicParameter = functionSymbol.hasVariadicParameter(); parameters.addAll(functionSymbol.parameters()); functionDefinitionLocation = functionSymbol.definitionLocation(); @@ -80,12 +89,13 @@ public class FunctionSymbolImpl extends SymbolImpl implements FunctionSymbol { } public FunctionSymbolImpl(String name, @Nullable String fullyQualifiedName, boolean hasVariadicParameter, - boolean isInstanceMethod, boolean hasDecorators, List parameters) { + boolean isInstanceMethod, boolean hasDecorators, List parameters, List decorators) { super(name, fullyQualifiedName); setKind(Kind.FUNCTION); this.hasVariadicParameter = hasVariadicParameter; this.isInstanceMethod = isInstanceMethod; this.hasDecorators = hasDecorators; + this.decorators = decorators; this.parameters.addAll(parameters); this.functionDefinitionLocation = null; this.isStub = true; @@ -114,7 +124,16 @@ private static boolean isInstanceMethod(FunctionDef functionDef) { List names = decorator.name().names(); return names.get(names.size() - 1).name(); }) - .noneMatch(decorator -> decorator.equals("staticmethod") || decorator.equals("classmethod")); + .noneMatch(decorator -> decorator.equals(STATIC_METHOD_DECORATOR) || decorator.equals(CLASS_METHOD_DECORATOR)); + } + + private static List decorators(FunctionDef functionDef) { + List decoratorNames = new ArrayList<>(); + for (Decorator decorator : functionDef.decorators()) { + String name = decorator.name().names().stream().map(Name::name).collect(Collectors.joining(".")); + decoratorNames.add(name); + } + return decoratorNames; } private void createParameterNames(List parameterTrees, @Nullable String fileId) { @@ -123,7 +142,7 @@ private void createParameterNames(List parameterTrees, @Nullable S if (anyParameter.is(Tree.Kind.PARAMETER)) { addParameter((org.sonar.plugins.python.api.tree.Parameter) anyParameter, fileId, parameterState); } else { - parameters.add(new ParameterImpl(null, false, parameterState, locationInFile(anyParameter, fileId))); + parameters.add(new ParameterImpl(null, InferredTypes.anyType(), false, parameterState, locationInFile(anyParameter, fileId))); } } } @@ -132,7 +151,12 @@ private void addParameter(org.sonar.plugins.python.api.tree.Parameter parameter, Name parameterName = parameter.name(); Token starToken = parameter.starToken(); if (parameterName != null) { - this.parameters.add(new ParameterImpl(parameterName.name(), parameter.defaultValue() != null, parameterState, locationInFile(parameter, fileId))); + TypeAnnotation typeAnnotation = parameter.typeAnnotation(); + InferredType declaredType = InferredTypes.anyType(); + if (typeAnnotation != null) { + declaredType = InferredTypes.declaredType(typeAnnotation); + } + this.parameters.add(new ParameterImpl(parameterName.name(), declaredType, parameter.defaultValue() != null, parameterState, locationInFile(parameter, fileId))); if (starToken != null) { hasVariadicParameter = true; } @@ -147,6 +171,11 @@ private void addParameter(org.sonar.plugins.python.api.tree.Parameter parameter, } } + @Override + public List decorators() { + return decorators; + } + private static class ParameterState { boolean keywordOnly = false; boolean positionalOnly = false; @@ -201,13 +230,15 @@ public void setOwner(Symbol owner) { private static class ParameterImpl implements Parameter { private final String name; + private final InferredType declaredType; private final boolean hasDefaultValue; private final boolean isKeywordOnly; private final boolean isPositionalOnly; private final LocationInFile location; - ParameterImpl(@Nullable String name, boolean hasDefaultValue, ParameterState parameterState, @Nullable LocationInFile location) { + ParameterImpl(@Nullable String name, InferredType declaredType, boolean hasDefaultValue, ParameterState parameterState, @Nullable LocationInFile location) { this.name = name; + this.declaredType = declaredType; this.hasDefaultValue = hasDefaultValue; this.isKeywordOnly = parameterState.keywordOnly; this.isPositionalOnly = parameterState.positionalOnly; @@ -220,6 +251,11 @@ public String name() { return name; } + @Override + public InferredType declaredType() { + return declaredType; + } + @Override public boolean hasDefaultValue() { return hasDefaultValue; diff --git a/python-frontend/src/main/java/org/sonar/python/semantic/Scope.java b/python-frontend/src/main/java/org/sonar/python/semantic/Scope.java index 73ea6c45a0..8f1216fbaf 100644 --- a/python-frontend/src/main/java/org/sonar/python/semantic/Scope.java +++ b/python-frontend/src/main/java/org/sonar/python/semantic/Scope.java @@ -38,6 +38,7 @@ import org.sonar.plugins.python.api.tree.Name; import org.sonar.plugins.python.api.tree.Parameter; import org.sonar.plugins.python.api.tree.Tree; +import org.sonar.python.tree.FunctionDefImpl; import org.sonar.python.types.InferredTypes; import org.sonar.python.types.TypeShed; @@ -116,6 +117,7 @@ void addFunctionSymbol(FunctionDef functionDef, @Nullable String fullyQualifiedN addBindingUsage(functionDef.name(), Usage.Kind.FUNC_DECLARATION, fullyQualifiedName); } else { FunctionSymbolImpl functionSymbol = new FunctionSymbolImpl(functionDef, fullyQualifiedName, pythonFile); + ((FunctionDefImpl) functionDef).setFunctionSymbol(functionSymbol); symbols.add(functionSymbol); symbolsByName.put(symbolName, functionSymbol); functionSymbol.addUsage(functionDef.name(), Usage.Kind.FUNC_DECLARATION); diff --git a/python-frontend/src/main/java/org/sonar/python/semantic/SymbolTableBuilder.java b/python-frontend/src/main/java/org/sonar/python/semantic/SymbolTableBuilder.java index 411d57df25..d6f66d5e90 100644 --- a/python-frontend/src/main/java/org/sonar/python/semantic/SymbolTableBuilder.java +++ b/python-frontend/src/main/java/org/sonar/python/semantic/SymbolTableBuilder.java @@ -39,6 +39,7 @@ import org.sonar.plugins.python.api.PythonFile; import org.sonar.plugins.python.api.symbols.AmbiguousSymbol; import org.sonar.plugins.python.api.symbols.ClassSymbol; +import org.sonar.plugins.python.api.symbols.FunctionSymbol; import org.sonar.plugins.python.api.symbols.Symbol; import org.sonar.plugins.python.api.symbols.Usage; import org.sonar.plugins.python.api.tree.AliasedName; @@ -619,8 +620,8 @@ public void visitClassDef(ClassDef pyClassDefTree) { scan(pyClassDefTree.decorators()); enterScope(pyClassDefTree); scan(pyClassDefTree.name()); - scan(pyClassDefTree.body()); resolveTypeHierarchy(pyClassDefTree, pyClassDefTree.name().symbol(), pythonFile, scopesByRootTree.get(fileInput).symbolsByName); + scan(pyClassDefTree.body()); leaveScope(); } @@ -663,15 +664,27 @@ private void addSymbolUsage(Name nameTree) { } } - /** - * Handle class member usages like the following: - *
-   *     class A:
-   *       foo = 42
-   *     print(A.foo)
-   * 
- */ private class ThirdPhaseVisitor extends BaseTreeVisitor { + + @Override + public void visitFunctionDef(FunctionDef functionDef) { + FunctionSymbol functionSymbol = ((FunctionDefImpl) functionDef).functionSymbol(); + ParameterList parameters = functionDef.parameters(); + if (functionSymbol != null && parameters != null) { + FunctionSymbolImpl functionSymbolImpl = (FunctionSymbolImpl) functionSymbol; + functionSymbolImpl.setParametersWithType(parameters); + } + super.visitFunctionDef(functionDef); + } + + /** + * Handle class member usages like the following: + *
+     *     class A:
+     *       foo = 42
+     *     print(A.foo)
+     * 
+ */ @Override public void visitQualifiedExpression(QualifiedExpression qualifiedExpression) { super.visitQualifiedExpression(qualifiedExpression); diff --git a/python-frontend/src/main/java/org/sonar/python/semantic/SymbolUtils.java b/python-frontend/src/main/java/org/sonar/python/semantic/SymbolUtils.java index 80544e5715..284ba2ac8e 100644 --- a/python-frontend/src/main/java/org/sonar/python/semantic/SymbolUtils.java +++ b/python-frontend/src/main/java/org/sonar/python/semantic/SymbolUtils.java @@ -257,10 +257,12 @@ public static Map> externalModulesSymbols() { ClassSymbolImpl flaskResponse = classSymbol("Response", "flask.Response", SET_COOKIE); - FunctionSymbolImpl makeResponse = new FunctionSymbolImpl("make_response", "flask.make_response", false, false, false, Collections.emptyList()); + FunctionSymbolImpl makeResponse = new FunctionSymbolImpl("make_response", "flask.make_response", false, false, false, Collections.emptyList(), + Collections.emptyList()); makeResponse.setDeclaredReturnType(InferredTypes.runtimeType(flaskResponse)); - FunctionSymbolImpl redirect = new FunctionSymbolImpl("redirect", "flask.redirect", false, false, false, Collections.emptyList()); + FunctionSymbolImpl redirect = new FunctionSymbolImpl("redirect", "flask.redirect", false, false, false, Collections.emptyList(), + Collections.emptyList()); redirect.setDeclaredReturnType(InferredTypes.runtimeType(flaskResponse)); globalSymbols.put("flask", new HashSet<>(Arrays.asList( diff --git a/python-frontend/src/main/java/org/sonar/python/types/AnyType.java b/python-frontend/src/main/java/org/sonar/python/types/AnyType.java index bd433407f5..b7ef74595f 100644 --- a/python-frontend/src/main/java/org/sonar/python/types/AnyType.java +++ b/python-frontend/src/main/java/org/sonar/python/types/AnyType.java @@ -50,4 +50,9 @@ public boolean canOnlyBe(String typeName) { public boolean canBeOrExtend(String typeName) { return true; } + + @Override + public boolean isCompatibleWith(InferredType other) { + return true; + } } diff --git a/python-frontend/src/main/java/org/sonar/python/types/InferredTypes.java b/python-frontend/src/main/java/org/sonar/python/types/InferredTypes.java index 3a6a9a165c..71df09e881 100644 --- a/python-frontend/src/main/java/org/sonar/python/types/InferredTypes.java +++ b/python-frontend/src/main/java/org/sonar/python/types/InferredTypes.java @@ -19,6 +19,7 @@ */ package org.sonar.python.types; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -65,6 +66,8 @@ public class InferredTypes { public static final InferredType BOOL = runtimeBuiltinType(BuiltinTypes.BOOL); + private static Map builtinSymbols; + private InferredTypes() { } @@ -86,6 +89,10 @@ public static InferredType runtimeType(@Nullable Symbol typeClass) { return anyType(); } + static void setBuiltinSymbols(Map builtinSymbols) { + InferredTypes.builtinSymbols = Collections.unmodifiableMap(builtinSymbols); + } + public static InferredType or(InferredType t1, InferredType t2) { return UnionType.or(t1, t2); } @@ -94,8 +101,12 @@ public static InferredType union(Stream types) { return types.reduce(InferredTypes::or).orElse(anyType()); } - public static InferredType declaredType(TypeAnnotation typeAnnotation, Map builtinSymbols) { - return declaredType(typeAnnotation.expression(), builtinSymbols); + public static InferredType declaredType(TypeAnnotation typeAnnotation) { + if (builtinSymbols != null) { + return declaredType(typeAnnotation.expression(), builtinSymbols); + } else { + return declaredType(typeAnnotation.expression(), Collections.emptyMap()); + } } private static InferredType declaredType(Expression expression, Map builtinSymbols) { diff --git a/python-frontend/src/main/java/org/sonar/python/types/RuntimeType.java b/python-frontend/src/main/java/org/sonar/python/types/RuntimeType.java index 2f99186fb4..19a77a1d51 100644 --- a/python-frontend/src/main/java/org/sonar/python/types/RuntimeType.java +++ b/python-frontend/src/main/java/org/sonar/python/types/RuntimeType.java @@ -71,6 +71,23 @@ public boolean canBeOrExtend(String typeName) { return typeClass.canBeOrExtend(typeName); } + @Override + public boolean isCompatibleWith(InferredType other) { + if (other instanceof RuntimeType) { + RuntimeType otherRuntimeType = (RuntimeType) other; + String otherFullyQualifiedName = otherRuntimeType.typeClass.fullyQualifiedName(); + boolean isDuckTypeCompatible = !otherRuntimeType.typeClass.declaredMembers().isEmpty() && + otherRuntimeType.typeClass.declaredMembers().stream().allMatch(m -> typeClass.resolveMember(m.name()).isPresent()); + boolean canBeOrExtend = otherFullyQualifiedName == null || this.canBeOrExtend(otherFullyQualifiedName); + return isDuckTypeCompatible || canBeOrExtend; + } + if (other instanceof UnionType) { + return ((UnionType) other).types().stream().anyMatch(this::isCompatibleWith); + } + // other is AnyType + return true; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/python-frontend/src/main/java/org/sonar/python/types/TypeShed.java b/python-frontend/src/main/java/org/sonar/python/types/TypeShed.java index 83b99ad48c..087e75397d 100644 --- a/python-frontend/src/main/java/org/sonar/python/types/TypeShed.java +++ b/python-frontend/src/main/java/org/sonar/python/types/TypeShed.java @@ -31,10 +31,12 @@ import java.util.stream.Collectors; import org.sonar.plugins.python.api.PythonFile; import org.sonar.plugins.python.api.symbols.ClassSymbol; +import org.sonar.plugins.python.api.symbols.FunctionSymbol; import org.sonar.plugins.python.api.symbols.Symbol; import org.sonar.plugins.python.api.tree.BaseTreeVisitor; import org.sonar.plugins.python.api.tree.FileInput; import org.sonar.plugins.python.api.tree.FunctionDef; +import org.sonar.plugins.python.api.tree.ParameterList; import org.sonar.plugins.python.api.tree.TypeAnnotation; import org.sonar.python.parser.PythonParser; import org.sonar.python.semantic.AmbiguousSymbolImpl; @@ -72,6 +74,7 @@ public static Map builtinSymbols() { builtins.put(globalVariable.fullyQualifiedName(), globalVariable); } TypeShed.builtins = Collections.unmodifiableMap(builtins); + InferredTypes.setBuiltinSymbols(builtins); fileInput.accept(new ReturnTypeVisitor()); TypeShed.builtinGlobalSymbols.put("", new HashSet<>(builtins.values())); } @@ -85,7 +88,7 @@ private static void setDeclaredReturnType(Symbol symbol, FunctionDef functionDef } if (symbol.is(Symbol.Kind.FUNCTION)) { FunctionSymbolImpl functionSymbol = (FunctionSymbolImpl) symbol; - functionSymbol.setDeclaredReturnType(InferredTypes.declaredType(returnTypeAnnotation, builtins)); + functionSymbol.setDeclaredReturnType(InferredTypes.declaredType(returnTypeAnnotation)); } else if (symbol.is(Symbol.Kind.AMBIGUOUS)) { Optional.ofNullable(((FunctionDefImpl) functionDef).functionSymbol()).ifPresent(functionSymbol -> setDeclaredReturnType(functionSymbol, functionDef)); } @@ -182,9 +185,29 @@ static class ReturnTypeVisitor extends BaseTreeVisitor { @Override public void visitFunctionDef(FunctionDef functionDef) { - Optional.ofNullable(functionDef.name().symbol()).ifPresent(symbol -> setDeclaredReturnType(symbol, functionDef)); + Optional.ofNullable(functionDef.name().symbol()).ifPresent(symbol -> { + setDeclaredReturnType(symbol, functionDef); + setParameterTypes(symbol, functionDef); + }); super.visitFunctionDef(functionDef); } + + private static void setParameterTypes(Symbol symbol, FunctionDef functionDef) { + if (symbol.is(Symbol.Kind.FUNCTION)) { + FunctionSymbolImpl functionSymbol = (FunctionSymbolImpl) symbol; + ParameterList parameters = functionDef.parameters(); + if (parameters != null) { + // For builtin functions, we don't have type information from typings.pyi for the parameters when constructing the initial symbol table + // We need to recreate those with that information + functionSymbol.setParametersWithType(parameters); + } + } else if (symbol.is(Symbol.Kind.AMBIGUOUS)) { + FunctionSymbol funcDefSymbol = ((FunctionDefImpl) functionDef).functionSymbol(); + if (funcDefSymbol != null) { + setParameterTypes(funcDefSymbol, functionDef); + } + } + } } } diff --git a/python-frontend/src/main/java/org/sonar/python/types/UnionType.java b/python-frontend/src/main/java/org/sonar/python/types/UnionType.java index 43ca0be127..8ec6de7eab 100644 --- a/python-frontend/src/main/java/org/sonar/python/types/UnionType.java +++ b/python-frontend/src/main/java/org/sonar/python/types/UnionType.java @@ -19,6 +19,7 @@ */ package org.sonar.python.types; +import java.util.Collections; import java.util.HashSet; import java.util.Objects; import java.util.Optional; @@ -87,6 +88,11 @@ public boolean canBeOrExtend(String typeName) { return types.stream().anyMatch(t -> t.canBeOrExtend(typeName)); } + @Override + public boolean isCompatibleWith(InferredType other) { + return types.stream().anyMatch(t -> t.isCompatibleWith(other)); + } + @Override public boolean equals(Object o) { if (this == o) { @@ -108,4 +114,8 @@ public int hashCode() { public String toString() { return "UnionType" + types; } + + Set types() { + return Collections.unmodifiableSet(types); + } } diff --git a/python-frontend/src/test/java/org/sonar/python/semantic/FunctionSymbolTest.java b/python-frontend/src/test/java/org/sonar/python/semantic/FunctionSymbolTest.java index e31612835b..c3d7f7ddaa 100644 --- a/python-frontend/src/test/java/org/sonar/python/semantic/FunctionSymbolTest.java +++ b/python-frontend/src/test/java/org/sonar/python/semantic/FunctionSymbolTest.java @@ -84,6 +84,9 @@ public void arity() { assertThat(functionSymbol.parameters().get(0).hasDefaultValue()).isFalse(); assertThat(functionSymbol.parameters().get(0).isKeywordOnly()).isFalse(); + functionSymbol = functionSymbol("def fn(p1: int): pass"); + assertThat(functionSymbol.parameters().get(0).declaredType().canOnlyBe("int")).isTrue(); + functionSymbol = functionSymbol("def fn(**kwargs): pass"); assertThat(functionSymbol.parameters()).hasSize(1); assertThat(functionSymbol.hasVariadicParameter()).isTrue(); @@ -97,7 +100,13 @@ public void arity() { functionSymbol = functionSymbol("class A:\n def method(self, p1): pass"); assertThat(functionSymbol.isInstanceMethod()).isTrue(); - functionSymbol = functionSymbol("class A:\n @staticmethod\n def method(self, p1): pass"); + functionSymbol = functionSymbol("class A:\n def method(*args, p1): pass"); + assertThat(functionSymbol.isInstanceMethod()).isTrue(); + + functionSymbol = functionSymbol("class A:\n @staticmethod\n def method((a, b), c): pass"); + assertThat(functionSymbol.isInstanceMethod()).isFalse(); + + functionSymbol = functionSymbol("class A:\n @staticmethod\n def method(p1, p2): pass"); assertThat(functionSymbol.isInstanceMethod()).isFalse(); functionSymbol = functionSymbol("class A:\n @classmethod\n def method(self, p1): pass"); diff --git a/python-frontend/src/test/java/org/sonar/python/types/AnyTypeTest.java b/python-frontend/src/test/java/org/sonar/python/types/AnyTypeTest.java index 36da48da98..c595a5362f 100644 --- a/python-frontend/src/test/java/org/sonar/python/types/AnyTypeTest.java +++ b/python-frontend/src/test/java/org/sonar/python/types/AnyTypeTest.java @@ -51,5 +51,7 @@ public void test_canOnlyBe() { @Test public void test_canBeOrExtend() { assertThat(ANY.canBeOrExtend("a")).isTrue(); + assertThat(InferredTypes.INT.isCompatibleWith(ANY)).isTrue(); + assertThat(ANY.isCompatibleWith(InferredTypes.INT)).isTrue(); } } diff --git a/python-frontend/src/test/java/org/sonar/python/types/InferredTypesTest.java b/python-frontend/src/test/java/org/sonar/python/types/InferredTypesTest.java index 2ba218f16b..307b73deb0 100644 --- a/python-frontend/src/test/java/org/sonar/python/types/InferredTypesTest.java +++ b/python-frontend/src/test/java/org/sonar/python/types/InferredTypesTest.java @@ -72,13 +72,13 @@ public void test_aliased_type_annotations() { "from typing import List", "l : List[int]" ); - assertThat(InferredTypes.declaredType(typeAnnotation, TypeShed.builtinSymbols())).isEqualTo(InferredTypes.LIST); + assertThat(InferredTypes.declaredType(typeAnnotation)).isEqualTo(InferredTypes.LIST); typeAnnotation = typeAnnotation( "from typing import Dict", "l : Dict[int, string]" ); - assertThat(InferredTypes.declaredType(typeAnnotation, TypeShed.builtinSymbols())).isEqualTo(InferredTypes.DICT); + assertThat(InferredTypes.declaredType(typeAnnotation)).isEqualTo(InferredTypes.DICT); } @Test @@ -87,25 +87,25 @@ public void test_union_type_annotations() { "from typing import Union", "l : Union[int, str]" ); - assertThat(InferredTypes.declaredType(typeAnnotation, TypeShed.builtinSymbols())).isEqualTo(InferredTypes.or(InferredTypes.INT, InferredTypes.STR)); + assertThat(InferredTypes.declaredType(typeAnnotation)).isEqualTo(InferredTypes.or(InferredTypes.INT, InferredTypes.STR)); typeAnnotation = typeAnnotation( "from typing import Union", "l : Union[int, str, bool]" ); - assertThat(InferredTypes.declaredType(typeAnnotation, TypeShed.builtinSymbols())).isEqualTo(InferredTypes.or(InferredTypes.or(InferredTypes.INT, InferredTypes.STR), InferredTypes.BOOL)); + assertThat(InferredTypes.declaredType(typeAnnotation)).isEqualTo(InferredTypes.or(InferredTypes.or(InferredTypes.INT, InferredTypes.STR), InferredTypes.BOOL)); typeAnnotation = typeAnnotation( "from typing import Union", "l : Union[Union[int, str], bool]" ); - assertThat(InferredTypes.declaredType(typeAnnotation, TypeShed.builtinSymbols())).isEqualTo(InferredTypes.or(InferredTypes.or(InferredTypes.INT, InferredTypes.STR), InferredTypes.BOOL)); + assertThat(InferredTypes.declaredType(typeAnnotation)).isEqualTo(InferredTypes.or(InferredTypes.or(InferredTypes.INT, InferredTypes.STR), InferredTypes.BOOL)); typeAnnotation = typeAnnotation( "from typing import Union", "l : Union[bool]" ); - assertThat(InferredTypes.declaredType(typeAnnotation, TypeShed.builtinSymbols())).isEqualTo(InferredTypes.BOOL); + assertThat(InferredTypes.declaredType(typeAnnotation)).isEqualTo(InferredTypes.BOOL); } @Test @@ -114,13 +114,13 @@ public void test_optional_type_annotations() { "from typing import Optional", "l : Optional[int]" ); - assertThat(InferredTypes.declaredType(typeAnnotation, TypeShed.builtinSymbols())).isEqualTo(InferredTypes.or(InferredTypes.INT, InferredTypes.NONE)); + assertThat(InferredTypes.declaredType(typeAnnotation)).isEqualTo(InferredTypes.or(InferredTypes.INT, InferredTypes.NONE)); typeAnnotation = typeAnnotation( "from typing import Optional", "l : Optional[int, string]" ); - assertThat(InferredTypes.declaredType(typeAnnotation, TypeShed.builtinSymbols())).isEqualTo(InferredTypes.anyType()); + assertThat(InferredTypes.declaredType(typeAnnotation)).isEqualTo(InferredTypes.anyType()); } private TypeAnnotation typeAnnotation(String... code) { diff --git a/python-frontend/src/test/java/org/sonar/python/types/RuntimeTypeTest.java b/python-frontend/src/test/java/org/sonar/python/types/RuntimeTypeTest.java index b2b5a7a232..e9cc113893 100644 --- a/python-frontend/src/test/java/org/sonar/python/types/RuntimeTypeTest.java +++ b/python-frontend/src/test/java/org/sonar/python/types/RuntimeTypeTest.java @@ -19,9 +19,11 @@ */ package org.sonar.python.types; +import java.util.Arrays; import java.util.Collections; import org.junit.Test; import org.sonar.python.semantic.ClassSymbolImpl; +import org.sonar.python.semantic.FunctionSymbolImpl; import org.sonar.python.semantic.SymbolImpl; import static java.util.Collections.singletonList; @@ -176,4 +178,38 @@ public void test_canBeOrExtend() { y.addSuperClass(new SymbolImpl("unknown", null)); assertThat(new RuntimeType(y).canBeOrExtend("z")).isTrue(); } + + @Test + public void test_isCompatibleWith() { + ClassSymbolImpl x1 = new ClassSymbolImpl("x1", "x1"); + ClassSymbolImpl x2 = new ClassSymbolImpl("x2", "x2"); + x2.addSuperClass(x1); + + assertThat(new RuntimeType(x2).isCompatibleWith(new RuntimeType(x1))).isTrue(); + assertThat(new RuntimeType(x1).isCompatibleWith(new RuntimeType(x1))).isTrue(); + assertThat(new RuntimeType(x1).isCompatibleWith(new RuntimeType(x2))).isFalse(); + + ClassSymbolImpl a = new ClassSymbolImpl("a", null); + ClassSymbolImpl b = new ClassSymbolImpl("b", "b"); + assertThat(new RuntimeType(a).isCompatibleWith(new RuntimeType(b))).isFalse(); + assertThat(new RuntimeType(b).isCompatibleWith(new RuntimeType(a))).isTrue(); + + ClassSymbolImpl y = new ClassSymbolImpl("y", "y"); + ClassSymbolImpl z = new ClassSymbolImpl("z", "z"); + y.addSuperClass(new SymbolImpl("unknown", null)); + assertThat(new RuntimeType(y).isCompatibleWith(new RuntimeType(z))).isTrue(); + + ClassSymbolImpl duck = new ClassSymbolImpl("duck", "duck"); + ClassSymbolImpl goose = new ClassSymbolImpl("goose", "goose"); + FunctionSymbolImpl duckSwim = new FunctionSymbolImpl("swim", "duck.swim", false, false, false, Collections.emptyList(), + Collections.emptyList()); + FunctionSymbolImpl duckQuack = new FunctionSymbolImpl("quack", "duck.quack", false, false, false, Collections.emptyList(), + Collections.emptyList()); + FunctionSymbolImpl gooseSwim = new FunctionSymbolImpl("swim", "goose.swim", false, false, false, Collections.emptyList(), + Collections.emptyList()); + duck.addMembers(Arrays.asList(duckSwim, duckQuack)); + goose.addMembers(Collections.singleton(gooseSwim)); + assertThat(new RuntimeType(duck).isCompatibleWith(new RuntimeType(goose))).isTrue(); + assertThat(new RuntimeType(goose).isCompatibleWith(new RuntimeType(duck))).isFalse(); + } } diff --git a/python-frontend/src/test/java/org/sonar/python/types/UnionTypeTest.java b/python-frontend/src/test/java/org/sonar/python/types/UnionTypeTest.java index 0fbd3bfe79..0445f539f7 100644 --- a/python-frontend/src/test/java/org/sonar/python/types/UnionTypeTest.java +++ b/python-frontend/src/test/java/org/sonar/python/types/UnionTypeTest.java @@ -122,4 +122,17 @@ public void test_canBeOrExtend() { assertThat(or(a, new RuntimeType(x2)).canBeOrExtend("x1")).isTrue(); } + @Test + public void test_isCompatibleWith() { + assertThat(a.isCompatibleWith(or(a, b))).isTrue(); + assertThat(or(a, b).isCompatibleWith(a)).isTrue(); + assertThat(c.isCompatibleWith(or(a, b))).isFalse(); + + ClassSymbolImpl x1 = new ClassSymbolImpl("x1", "x1"); + ClassSymbolImpl x2 = new ClassSymbolImpl("x2", "x2"); + x2.addSuperClass(x1); + assertThat(or(a, new RuntimeType(x2)).isCompatibleWith(new RuntimeType(x1))).isTrue(); + assertThat(new RuntimeType(x1).isCompatibleWith(or(a, new RuntimeType(x2)))).isFalse(); + } + }