diff --git a/python/src/com/jetbrains/python/PyBundle.properties b/python/src/com/jetbrains/python/PyBundle.properties
index b9d90a935e51..81024d6e41cc 100644
--- a/python/src/com/jetbrains/python/PyBundle.properties
+++ b/python/src/com/jetbrains/python/PyBundle.properties
@@ -87,6 +87,9 @@ QFIX.replace.function.set.with.literal=Replace function call with set literal
#DocstringQuickFix
QFIX.docstring=Add/remove parameters docstring parameters
+#RemoveDecoratorQuickFix
+QFIX.remove.decorator=Remove decorator
+
# Intentions: INTN
INTN.Family.convert.import.unqualify=Convert 'import module' to 'from module import'
INTN.Family.convert.import.qualify=Convert 'from module import' to 'import module'
@@ -393,6 +396,10 @@ INSP.NAME.missing.super.constructor=Missed call to constructor of super class
# PySetFunctionToLiteralInspection
INSP.NAME.set.function.to.literal=Function call can be replaced with set literal
+# PyDecoratorInspection
+INSP.NAME.decorator.outside.class=Class specific decorator on method outside class
+
+
# Refactoring
# introduce
refactoring.introduce.name.error=Incorrect name
diff --git a/python/src/com/jetbrains/python/actions/RemoveDecoratorQuickFix.java b/python/src/com/jetbrains/python/actions/RemoveDecoratorQuickFix.java
new file mode 100644
index 000000000000..ca605c59b364
--- /dev/null
+++ b/python/src/com/jetbrains/python/actions/RemoveDecoratorQuickFix.java
@@ -0,0 +1,30 @@
+package com.jetbrains.python.actions;
+
+import com.intellij.codeInspection.LocalQuickFix;
+import com.intellij.codeInspection.ProblemDescriptor;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.jetbrains.python.PyBundle;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * User: catherine
+ *
+ * QuickFix to remove redundant argument equal default
+ */
+public class RemoveDecoratorQuickFix implements LocalQuickFix {
+ @NotNull
+ public String getName() {
+ return PyBundle.message("QFIX.remove.decorator");
+ }
+
+ @NotNull
+ public String getFamilyName() {
+ return PyBundle.message("INSP.GROUP.python");
+ }
+
+ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
+ PsiElement element = descriptor.getPsiElement();
+ element.delete();
+ }
+}
diff --git a/python/src/com/jetbrains/python/inspections/PyDecoratorInspection.java b/python/src/com/jetbrains/python/inspections/PyDecoratorInspection.java
new file mode 100644
index 000000000000..e9ec9f07f99f
--- /dev/null
+++ b/python/src/com/jetbrains/python/inspections/PyDecoratorInspection.java
@@ -0,0 +1,56 @@
+package com.jetbrains.python.inspections;
+
+import com.intellij.codeInspection.ProblemsHolder;
+import com.intellij.psi.PsiElementVisitor;
+import com.jetbrains.python.PyBundle;
+import com.jetbrains.python.actions.RemoveDecoratorQuickFix;
+import com.jetbrains.python.psi.PyClass;
+import com.jetbrains.python.psi.PyDecorator;
+import com.jetbrains.python.psi.PyDecoratorList;
+import com.jetbrains.python.psi.PyFunction;
+import org.jetbrains.annotations.Nls;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * User: catherine
+ *
+ * Inspection to detect occurrences of @classmethod and @staticmethod
+ * on methods outside of a class
+ */
+public class PyDecoratorInspection extends PyInspection {
+ @Nls
+ @NotNull
+ @Override
+ public String getDisplayName() {
+ return PyBundle.message("INSP.NAME.too.broad.exception.clauses");
+ }
+
+ @NotNull
+ @Override
+ public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
+ return new Visitor(holder);
+ }
+
+ private static class Visitor extends PyInspectionVisitor {
+
+ public Visitor(final ProblemsHolder holder) {
+ super(holder);
+ }
+
+ @Override
+ public void visitPyFunction(final PyFunction node){
+ PyClass containingClass = node.getContainingClass();
+ if (containingClass != null)
+ return;
+
+ PyDecoratorList decorators = node.getDecoratorList();
+ if (decorators == null)
+ return;
+ for (PyDecorator decorator : decorators.getDecorators()) {
+ String name = decorator.getText();
+ if (name.equals("@classmethod") || name.equals("@staticmethod"))
+ registerProblem(decorator, "Decorator " + name + " on method outside class", new RemoveDecoratorQuickFix());
+ }
+ }
+ }
+}
diff --git a/python/src/com/jetbrains/python/inspections/PythonInspectionToolProvider.java b/python/src/com/jetbrains/python/inspections/PythonInspectionToolProvider.java
index bc152a531797..86a330116336 100644
--- a/python/src/com/jetbrains/python/inspections/PythonInspectionToolProvider.java
+++ b/python/src/com/jetbrains/python/inspections/PythonInspectionToolProvider.java
@@ -59,6 +59,7 @@ public class PythonInspectionToolProvider implements InspectionToolProvider {
PyMissingConstructorInspection.class,
PyArgumentEqualDefaultInspection.class,
PySetFunctionToLiteralInspection.class,
+ PyDecoratorInspection.class,
};
}
}
diff --git a/python/testData/inspections/PyDecoratorInspection/test.py b/python/testData/inspections/PyDecoratorInspection/test.py
new file mode 100644
index 000000000000..bea3759b2ef4
--- /dev/null
+++ b/python/testData/inspections/PyDecoratorInspection/test.py
@@ -0,0 +1,8 @@
+class C:
+ @classmethod
+ def foo(self):
+ pass
+
+@classmethod
+def foo(self):
+ print ("Constructor C was called")
\ No newline at end of file
diff --git a/python/testData/inspections/RemoveDecorator.py b/python/testData/inspections/RemoveDecorator.py
new file mode 100644
index 000000000000..c9fc9cb2e6af
--- /dev/null
+++ b/python/testData/inspections/RemoveDecorator.py
@@ -0,0 +1,8 @@
+class C:
+ @classmethod
+ def foo(self):
+ pass
+
+@classmethod
+def foo(self):
+ print ("Constructor C was called")
\ No newline at end of file
diff --git a/python/testData/inspections/RemoveDecorator_after.py b/python/testData/inspections/RemoveDecorator_after.py
new file mode 100644
index 000000000000..cc9672620d6f
--- /dev/null
+++ b/python/testData/inspections/RemoveDecorator_after.py
@@ -0,0 +1,7 @@
+class C:
+ @classmethod
+ def foo(self):
+ pass
+
+def foo(self):
+ print ("Constructor C was called")
\ No newline at end of file
diff --git a/python/testSrc/com/jetbrains/python/PyQuickFixTest.java b/python/testSrc/com/jetbrains/python/PyQuickFixTest.java
index fd87d91cb931..b865bb6c38c1 100644
--- a/python/testSrc/com/jetbrains/python/PyQuickFixTest.java
+++ b/python/testSrc/com/jetbrains/python/PyQuickFixTest.java
@@ -252,6 +252,11 @@ public class PyQuickFixTest extends PyLightFixtureTestCase {
PyBundle.message("QFIX.remove.argument.equal.default"), true, true);
}
+ public void testRemoveDecorator() { //PY-3348
+ doInspectionTest("RemoveDecorator.py", PyDecoratorInspection.class,
+ PyBundle.message("QFIX.remove.decorator"), true, true);
+ }
+
public void testSetFunctionToLiteral() { //PY-3120
setLanguageLevel(LanguageLevel.PYTHON27);
doInspectionTest("SetFunctionToLiteral.py", PySetFunctionToLiteralInspection.class,
diff --git a/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java b/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java
index 87c697b6630b..6ada8f984433 100644
--- a/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java
+++ b/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java
@@ -344,4 +344,8 @@ public class PythonInspectionsTest extends PyLightFixtureTestCase {
setLanguageLevel(LanguageLevel.PYTHON27);
doHighlightingTest(PySetFunctionToLiteralInspection.class);
}
+
+ public void testPyDecoratorInspection() { //PY-3348
+ doHighlightingTest(PyDecoratorInspection.class);
+ }
}