mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
committed by
Semyon Proshev
parent
5675e09b0b
commit
0ee5f9152d
@@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user