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);
+ }
+ );
+ }
+}