From 8db40ed959ac5d90f986b61544fbebef2e62ebee Mon Sep 17 00:00:00 2001 From: Maksim Grebeniuk <122789225+maksim-grebeniuk-sonarsource@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:50:59 +0100 Subject: [PATCH] SONARPY-2290 Support decorators for FunctionType to descriptors converter (#2125) --- .../checks/ChangeMethodContractCheck.java | 1 + .../resources/checks/changeMethodContract.py | 19 +++++++++ ...nctionDescriptorToPythonTypeConverter.java | 3 -- .../PythonTypeToDescriptorConverter.java | 17 ++++++-- .../v2/ProjectLevelTypeTableTest.java | 41 ++++++++++++++++--- .../python/types/v2/FunctionTypeTest.java | 1 - 6 files changed, 69 insertions(+), 13 deletions(-) diff --git a/python-checks/src/main/java/org/sonar/python/checks/ChangeMethodContractCheck.java b/python-checks/src/main/java/org/sonar/python/checks/ChangeMethodContractCheck.java index aa04161899..ffab09d475 100644 --- a/python-checks/src/main/java/org/sonar/python/checks/ChangeMethodContractCheck.java +++ b/python-checks/src/main/java/org/sonar/python/checks/ChangeMethodContractCheck.java @@ -43,6 +43,7 @@ public class ChangeMethodContractCheck extends PythonSubscriptionCheck { private static final Set IGNORING_DECORATORS = Set.of( + "abc.abstractmethod", "abstractmethod", "overload" ); diff --git a/python-checks/src/test/resources/checks/changeMethodContract.py b/python-checks/src/test/resources/checks/changeMethodContract.py index 8a797e13f3..1e3b004f46 100644 --- a/python-checks/src/test/resources/checks/changeMethodContract.py +++ b/python-checks/src/test/resources/checks/changeMethodContract.py @@ -129,3 +129,22 @@ def tzname(self): # Noncompliant class MyDictionary(dict): def get(self, key): ... + +from abc import abstractmethod +import abc + +class AbstractSuperclass: + @abstractmethod + def foo(self, a: int): + ... + + @abc.abstractmethod + def bar(self, a: int): + ... + +class InheritedFromAbstractSuperclass(AbstractSuperclass): + def foo(self): # Noncompliant + ... + + def bar(self): # Noncompliant + ... diff --git a/python-frontend/src/main/java/org/sonar/python/semantic/v2/converter/FunctionDescriptorToPythonTypeConverter.java b/python-frontend/src/main/java/org/sonar/python/semantic/v2/converter/FunctionDescriptorToPythonTypeConverter.java index ecda74f9df..b5456c90ed 100644 --- a/python-frontend/src/main/java/org/sonar/python/semantic/v2/converter/FunctionDescriptorToPythonTypeConverter.java +++ b/python-frontend/src/main/java/org/sonar/python/semantic/v2/converter/FunctionDescriptorToPythonTypeConverter.java @@ -55,9 +55,6 @@ public PythonType convert(ConversionContext ctx, FunctionDescriptor from) { var decorators = from.decorators() .stream() - .map(decoratorName -> Stream.of(ctx.moduleFqn(), decoratorName) - .filter(Predicate.not(String::isEmpty)) - .collect(Collectors.joining("."))) .map(ctx.lazyTypesContext()::getOrCreateLazyType) .map(TypeWrapper::of) .toList(); diff --git a/python-frontend/src/main/java/org/sonar/python/semantic/v2/converter/PythonTypeToDescriptorConverter.java b/python-frontend/src/main/java/org/sonar/python/semantic/v2/converter/PythonTypeToDescriptorConverter.java index a03d306ea3..22edec36c2 100644 --- a/python-frontend/src/main/java/org/sonar/python/semantic/v2/converter/PythonTypeToDescriptorConverter.java +++ b/python-frontend/src/main/java/org/sonar/python/semantic/v2/converter/PythonTypeToDescriptorConverter.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -36,6 +37,7 @@ import org.sonar.python.types.v2.FunctionType; import org.sonar.python.types.v2.ParameterV2; import org.sonar.python.types.v2.PythonType; +import org.sonar.python.types.v2.TypeWrapper; import org.sonar.python.types.v2.UnionType; import org.sonar.python.types.v2.UnknownType; @@ -87,13 +89,20 @@ private static Descriptor convert(String moduleFqn, FunctionType type) { .map(parameter -> convert(moduleFqn, parameter)) .toList(); + var decorators = type.decorators() + .stream() + .map(TypeWrapper::type) + .map(decorator -> typeFqn(moduleFqn, decorator)) + .filter(Objects::nonNull) + .toList(); + // Using FunctionType#name and FunctionType#fullyQualifiedName instead of symbol is only accurate if the function has not been reassigned // This logic should be revisited when tackling SONARPY-2285 return new FunctionDescriptor(type.name(), type.fullyQualifiedName(), parameters, type.isAsynchronous(), type.isInstanceMethod(), - List.of(), + decorators, type.hasDecorators(), type.definitionLocation().orElse(null), null, @@ -167,8 +176,10 @@ private static FunctionDescriptor.Parameter convert(String moduleFqn, ParameterV private static String typeFqn(String moduleFqn, PythonType type) { if (type instanceof UnknownType.UnresolvedImportType importType) { return importType.importPath(); - } else if (type instanceof ClassType classType) { - return moduleFqn + "." + classType.name(); + } else if (type instanceof ClassType) { + return moduleFqn + "." + type.name(); + } else if (type instanceof FunctionType functionType) { + return functionType.fullyQualifiedName(); } return null; } diff --git a/python-frontend/src/test/java/org/sonar/python/semantic/v2/ProjectLevelTypeTableTest.java b/python-frontend/src/test/java/org/sonar/python/semantic/v2/ProjectLevelTypeTableTest.java index 0297aa43ea..8e3268f0e2 100644 --- a/python-frontend/src/test/java/org/sonar/python/semantic/v2/ProjectLevelTypeTableTest.java +++ b/python-frontend/src/test/java/org/sonar/python/semantic/v2/ProjectLevelTypeTableTest.java @@ -20,7 +20,6 @@ package org.sonar.python.semantic.v2; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.sonar.plugins.python.api.PythonFile; import org.sonar.plugins.python.api.tree.ExpressionStatement; @@ -267,7 +266,6 @@ void resolveStubsWithImportedModuleVariableDescriptor() { } @Test - @Disabled("SONARPY-2290") void importFunctionWithDecorators() { var projectLevelSymbolTable = new ProjectLevelSymbolTable(); var libTree = parseWithoutSymbols( @@ -294,7 +292,6 @@ def foo(): ... } @Test - @Disabled("SONARPY-2290") void importFunctionWithImportedDecorators() { var projectLevelSymbolTable = new ProjectLevelSymbolTable(); var libTree = parseWithoutSymbols( @@ -305,9 +302,9 @@ def lib_decorator(): ... projectLevelSymbolTable.addModule(libTree, "", pythonFile("lib.py")); var lib2Tree = parseWithoutSymbols( """ - import lib + import lib as l - @lib.lib_decorator + @l.lib_decorator def foo(): ... """ ); @@ -322,7 +319,39 @@ def foo(): ... ); var fooType = (FunctionType) ((ExpressionStatement) fileInput.statements().statements().get(1)).expressions().get(0).typeV2(); var typeWrapper = (LazyTypeWrapper) fooType.decorators().get(0); - assertThat(typeWrapper.hasImportPath("lib2.lib.lib_decorator")).isTrue(); + assertThat(typeWrapper.hasImportPath("lib.lib_decorator")).isTrue(); + } + + @Test + void importedFunctionDecoratorNamesTest() { + var projectLevelSymbolTable = new ProjectLevelSymbolTable(); + var libTree = parseWithoutSymbols( + """ + from abc import abstractmethod + + class A: + @abstractmethod + def foo(self): ... + """ + ); + projectLevelSymbolTable.addModule(libTree, "", pythonFile("lib.py")); + + var projectLevelTypeTable = new ProjectLevelTypeTable(projectLevelSymbolTable); + var mainFile = pythonFile("main.py"); + var fileInput = parseAndInferTypes(projectLevelTypeTable, mainFile, """ + from lib import A + from datetime import tzinfo + A.foo + tzinfo.tzname + """ + ); + var fooType = (FunctionType) ((ExpressionStatement) fileInput.statements().statements().get(2)).expressions().get(0).typeV2(); + var typeWrapper = (LazyTypeWrapper) fooType.decorators().get(0); + assertThat(typeWrapper.hasImportPath("abc.abstractmethod")).isTrue(); + var tznameType = (FunctionType) ((ExpressionStatement) fileInput.statements().statements().get(3)).expressions().get(0).typeV2(); + typeWrapper = (LazyTypeWrapper) tznameType.decorators().get(0); + // SONARPY-2300 - need to fix serializer to use fully qualified names + assertThat(typeWrapper.hasImportPath("abstractmethod")).isTrue(); } @Test diff --git a/python-frontend/src/test/java/org/sonar/python/types/v2/FunctionTypeTest.java b/python-frontend/src/test/java/org/sonar/python/types/v2/FunctionTypeTest.java index 3e126e02b7..5cb5bfaa8d 100644 --- a/python-frontend/src/test/java/org/sonar/python/types/v2/FunctionTypeTest.java +++ b/python-frontend/src/test/java/org/sonar/python/types/v2/FunctionTypeTest.java @@ -191,7 +191,6 @@ void declared_return_type() { @Test void decorators() { - // TODO: SONARPY-1772 Handle decorators FunctionType functionType = functionType("@something\ndef fn(p1, *args): pass"); assertThat(functionType.hasDecorators()).isTrue();