From 0ee5f9152d2533722b38c899b67d1d96a63413ed Mon Sep 17 00:00:00 2001 From: Semyon Proshev Date: Fri, 12 May 2017 22:57:48 +0300 Subject: [PATCH] PY-22971 Fixed: Support @typing.overload in regular Python files, not only in Python stubs Introduce PyOverloadsInspection to check that implementation exists and is located after overloads. The inspection also raise warning when overload signature is not compatible with the implementation. --- .../PyOverloadsInspection.html | 5 + python/src/META-INF/python-core-common.xml | 1 + .../inspections/PyOverloadsInspection.kt | 124 ++++++++++++++++++ .../PyOverloadsInspection/correct.py | 37 ++++++ .../PyOverloadsInspection/incompatible.py | 28 ++++ .../PyOverloadsInspection/noImplementation.py | 30 +++++ .../overloadAfterImplementation.py | 37 ++++++ .../PyOverloadsInspectionTest.java | 54 ++++++++ 8 files changed, 316 insertions(+) create mode 100644 python/resources/inspectionDescriptions/PyOverloadsInspection.html create mode 100644 python/src/com/jetbrains/python/inspections/PyOverloadsInspection.kt create mode 100644 python/testData/inspections/PyOverloadsInspection/correct.py create mode 100644 python/testData/inspections/PyOverloadsInspection/incompatible.py create mode 100644 python/testData/inspections/PyOverloadsInspection/noImplementation.py create mode 100644 python/testData/inspections/PyOverloadsInspection/overloadAfterImplementation.py create mode 100644 python/testSrc/com/jetbrains/python/inspections/PyOverloadsInspectionTest.java diff --git a/python/resources/inspectionDescriptions/PyOverloadsInspection.html b/python/resources/inspectionDescriptions/PyOverloadsInspection.html new file mode 100644 index 000000000000..2fe9259e716e --- /dev/null +++ b/python/resources/inspectionDescriptions/PyOverloadsInspection.html @@ -0,0 +1,5 @@ + + +This inspection validates overloads in regular Python files. + + \ No newline at end of file diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index e532cce0b628..1fcd94f60f08 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -403,6 +403,7 @@ + diff --git a/python/src/com/jetbrains/python/inspections/PyOverloadsInspection.kt b/python/src/com/jetbrains/python/inspections/PyOverloadsInspection.kt new file mode 100644 index 000000000000..04a1c9a969f3 --- /dev/null +++ b/python/src/com/jetbrains/python/inspections/PyOverloadsInspection.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.inspections + +import com.intellij.codeInspection.LocalInspectionToolSession +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElementVisitor +import com.intellij.util.Processor +import com.intellij.util.containers.SortedList +import com.jetbrains.python.codeInsight.controlflow.ScopeOwner +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyFile +import com.jetbrains.python.psi.PyFunction +import com.jetbrains.python.psi.PyUtil +import com.jetbrains.python.pyi.PyiFile +import com.jetbrains.python.pyi.PyiTypeProvider +import java.util.* + +class PyOverloadsInspection : PyInspection() { + + override fun buildVisitor(holder: ProblemsHolder, + isOnTheFly: Boolean, + session: LocalInspectionToolSession): PsiElementVisitor = Visitor(holder, session) + + private class Visitor(holder: ProblemsHolder, session: LocalInspectionToolSession) : PyInspectionVisitor(holder, session) { + + override fun visitPyClass(node: PyClass?) { + if (node?.containingFile is PyiFile) return + + super.visitPyClass(node) + + if (node != null) { + processScope(node, { node.visitMethods(it, false, myTypeEvalContext) }) + } + } + + override fun visitPyFile(node: PyFile?) { + if (node is PyiFile) return + + super.visitPyFile(node) + + if (node != null) { + processScope(node, { processor -> node.topLevelFunctions.forEach { processor.process(it) } }) + } + } + + private fun processScope(owner: ScopeOwner, processorUsage: (GroupingFunctionsByNameProcessor) -> Unit) { + val processor = GroupingFunctionsByNameProcessor() + processorUsage(processor) + processor.result.values.forEach { processSameNameFunctions(owner, it) } + } + + private fun processSameNameFunctions(owner: ScopeOwner, functions: List) { + if (functions.find { PyiTypeProvider.isOverload(it, myTypeEvalContext) } == null) return + + val implementation = functions.lastOrNull { !PyiTypeProvider.isOverload(it, myTypeEvalContext) } + + if (implementation == null) { + functions + .maxBy { it.textOffset } + ?.let { + registerProblem(it.nameIdentifier, + "A series of @overload-decorated ${chooseBetweenFunctionsAndMethods(owner)} " + + "should always be followed by an implementation that is not @overload-ed") + } + } + else { + if (implementation != functions.last()) { + registerProblem(functions.last().nameIdentifier, + "A series of @overload-decorated ${chooseBetweenFunctionsAndMethods(owner)} " + + "should always be followed by an implementation that is not @overload-ed") + } + + functions + .asSequence() + .filter { isIncompatibleOverload(implementation, it) } + .forEach { + registerProblem(it.nameIdentifier, + "Signature of this @overload-decorated ${chooseBetweenFunctionAndMethod(owner)} " + + "is not compatible with the implementation") + } + } + } + + private fun chooseBetweenFunctionsAndMethods(owner: ScopeOwner) = if (owner is PyClass) "methods" else "functions" + + private fun chooseBetweenFunctionAndMethod(owner: ScopeOwner) = if (owner is PyClass) "method" else "function" + + private fun isIncompatibleOverload(implementation: PyFunction, overload: PyFunction): Boolean { + return implementation != overload && + PyiTypeProvider.isOverload(overload, myTypeEvalContext) && + !PyUtil.isSignatureCompatibleTo(implementation, overload, myTypeEvalContext) + } + } + + private class GroupingFunctionsByNameProcessor : Processor { + + val result: MutableMap> = HashMap() + + override fun process(t: PyFunction?): Boolean { + val name = t?.name + if (name != null) { + result + .getOrPut(name, { SortedList { f1, f2 -> f1.textOffset - f2.textOffset } }) + .add(t!!) + } + + return true + } + } +} \ No newline at end of file diff --git a/python/testData/inspections/PyOverloadsInspection/correct.py b/python/testData/inspections/PyOverloadsInspection/correct.py new file mode 100644 index 000000000000..b0422f80ea34 --- /dev/null +++ b/python/testData/inspections/PyOverloadsInspection/correct.py @@ -0,0 +1,37 @@ +from typing import overload + + +@overload +def foo(value: None) -> None: + pass + + +@overload +def foo(value: int) -> str: + pass + + +@overload +def foo(value: str) -> str: + pass + + +def foo(value): + return None + + +class A: + @overload + def foo(self, value: None) -> None: + pass + + @overload + def foo(self, value: int) -> str: + pass + + @overload + def foo(self, value: str) -> str: + pass + + def foo(self, value): + return None \ No newline at end of file diff --git a/python/testData/inspections/PyOverloadsInspection/incompatible.py b/python/testData/inspections/PyOverloadsInspection/incompatible.py new file mode 100644 index 000000000000..0a20a97a54c4 --- /dev/null +++ b/python/testData/inspections/PyOverloadsInspection/incompatible.py @@ -0,0 +1,28 @@ +from typing import overload + + +@overload +def foo() -> None: + pass + + +@overload +def foo(value: str) -> str: + pass + + +def foo(value): + return None + + +class A: + @overload + def foo(self) -> None: + pass + + @overload + def foo(self, value: str) -> str: + pass + + def foo(self, value): + return None \ No newline at end of file diff --git a/python/testData/inspections/PyOverloadsInspection/noImplementation.py b/python/testData/inspections/PyOverloadsInspection/noImplementation.py new file mode 100644 index 000000000000..c354030927c8 --- /dev/null +++ b/python/testData/inspections/PyOverloadsInspection/noImplementation.py @@ -0,0 +1,30 @@ +from typing import overload + + +@overload +def foo(value: None) -> None: + pass + + +@overload +def foo(value: int) -> str: + pass + + +@overload +def foo(value: str) -> str: + pass + + +class A: + @overload + def foo(self, value: None) -> None: + pass + + @overload + def foo(self, value: int) -> str: + pass + + @overload + def foo(self, value: str) -> str: + pass \ No newline at end of file diff --git a/python/testData/inspections/PyOverloadsInspection/overloadAfterImplementation.py b/python/testData/inspections/PyOverloadsInspection/overloadAfterImplementation.py new file mode 100644 index 000000000000..278cb52b2d2c --- /dev/null +++ b/python/testData/inspections/PyOverloadsInspection/overloadAfterImplementation.py @@ -0,0 +1,37 @@ +from typing import overload + + +@overload +def foo(value: None) -> None: + pass + + +@overload +def foo(value: int) -> str: + pass + + +def foo(value): + return None + + +@overload +def foo(value: str) -> str: + pass + + +class A: + @overload + def foo(self, value: None) -> None: + pass + + @overload + def foo(self, value: int) -> str: + pass + + def foo(self, value): + return None + + @overload + def foo(self, value: str) -> str: + pass \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/inspections/PyOverloadsInspectionTest.java b/python/testSrc/com/jetbrains/python/inspections/PyOverloadsInspectionTest.java new file mode 100644 index 000000000000..409ecfbdc4ad --- /dev/null +++ b/python/testSrc/com/jetbrains/python/inspections/PyOverloadsInspectionTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.inspections; + +import com.jetbrains.python.fixtures.PyTestCase; +import com.jetbrains.python.psi.LanguageLevel; + +public class PyOverloadsInspectionTest extends PyTestCase { + + // PY-22971 + public void testCorrect() { + doTest(); + } + + // PY-22971 + public void testNoImplementation() { + doTest(); + } + + // PY-22971 + public void testOverloadAfterImplementation() { + doTest(); + } + + // PY-22971 + public void testIncompatible() { + doTest(); + } + + private void doTest() { + runWithLanguageLevel( + LanguageLevel.PYTHON35, + () -> { + final String path = "inspections/PyOverloadsInspection/" + getTestName(true) + ".py"; + myFixture.configureByFile(path); + myFixture.enableInspections(PyOverloadsInspection.class); + myFixture.checkHighlighting(true, false, true); + } + ); + } +}