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.
This commit is contained in:
Semyon Proshev
2017-05-13 00:17:47 +03:00
committed by Semyon Proshev
parent 5675e09b0b
commit 0ee5f9152d
8 changed files with 316 additions and 0 deletions
@@ -0,0 +1,5 @@
<html>
<body>
This inspection validates overloads in regular Python files.
</body>
</html>
@@ -403,6 +403,7 @@
<localInspection language="Python" shortName="PyAssignmentToLoopOrWithParameterInspection" suppressId="PyAssignmentToLoopOrWithParameter" displayName="Assignment to 'for' loop or 'with' statement parameter" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyAssignmentToLoopOrWithParameterInspection"/>
<localInspection language="Python" shortName="PyDunderSlotsInspection" suppressId="PyDunderSlots" displayName="Definition of __slots__ in a class" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyDunderSlotsInspection"/>
<localInspection language="Python" shortName="PyMissingTypeHintsInspection" suppressId="PyMissingTypeHints" displayName="Missing type hinting for function definition" groupKey="INSP.GROUP.python" enabledByDefault="false" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyMissingTypeHintsInspection"/>
<localInspection language="Python" shortName="PyOverloadsInspection" suppressId="PyOverloads" displayName="Overloads in regular Python files" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyOverloadsInspection"/>
<defaultLiveTemplatesProvider implementation="com.jetbrains.python.codeInsight.liveTemplates.PyDefaultLiveTemplatesProvider"/>
<liveTemplateContext implementation="com.jetbrains.python.codeInsight.liveTemplates.PythonTemplateContextType$General"/>
@@ -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<PyFunction>) {
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<PyFunction> {
val result: MutableMap<String, MutableList<PyFunction>> = HashMap()
override fun process(t: PyFunction?): Boolean {
val name = t?.name
if (name != null) {
result
.getOrPut(name, { SortedList<PyFunction> { f1, f2 -> f1.textOffset - f2.textOffset } })
.add(t!!)
}
return true
}
}
}
@@ -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
@@ -0,0 +1,28 @@
from typing import overload
@overload
def <warning descr="Signature of this @overload-decorated function is not compatible with the implementation">foo</warning>() -> None:
pass
@overload
def foo(value: str) -> str:
pass
def foo(value):
return None
class A:
@overload
def <warning descr="Signature of this @overload-decorated method is not compatible with the implementation">foo</warning>(self) -> None:
pass
@overload
def foo(self, value: str) -> str:
pass
def foo(self, value):
return None
@@ -0,0 +1,30 @@
from typing import overload
@overload
def foo(value: None) -> None:
pass
@overload
def foo(value: int) -> str:
pass
@overload
def <warning descr="A series of @overload-decorated functions should always be followed by an implementation that is not @overload-ed">foo</warning>(value: str) -> str:
pass
class A:
@overload
def foo(self, value: None) -> None:
pass
@overload
def foo(self, value: int) -> str:
pass
@overload
def <warning descr="A series of @overload-decorated methods should always be followed by an implementation that is not @overload-ed">foo</warning>(self, value: str) -> str:
pass
@@ -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 <warning descr="A series of @overload-decorated functions should always be followed by an implementation that is not @overload-ed">foo</warning>(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 <warning descr="A series of @overload-decorated methods should always be followed by an implementation that is not @overload-ed">foo</warning>(self, value: str) -> str:
pass
@@ -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);
}
);
}
}