PY-20280 Fixed: Warn if class variable listed in __slots__
authorSemyon Proshev <Semyon.Proshev@jetbrains.com>
Fri, 5 Aug 2016 14:10:50 +0000 (17:10 +0300)
committerSemyon Proshev <Semyon.Proshev@jetbrains.com>
Wed, 10 Aug 2016 14:29:49 +0000 (17:29 +0300)
Introduce PyDunderSlotsInspection which detects invalid definition of __slots__ in a class

python/resources/inspectionDescriptions/PyDunderSlotsInspection.html [new file with mode: 0644]
python/src/META-INF/python-core-common.xml
python/src/com/jetbrains/python/inspections/PyDunderSlotsInspection.kt [new file with mode: 0644]
python/testData/inspections/PyDunderSlotsInspection/test.py [new file with mode: 0644]
python/testSrc/com/jetbrains/python/PythonInspectionsTest.java

diff --git a/python/resources/inspectionDescriptions/PyDunderSlotsInspection.html b/python/resources/inspectionDescriptions/PyDunderSlotsInspection.html
new file mode 100644 (file)
index 0000000..e742af8
--- /dev/null
@@ -0,0 +1,5 @@
+<html>
+<body>
+This inspection detects invalid definition of __slots__ in a class.
+</body>
+</html>
\ No newline at end of file
index f1415df9cf21fa7d5a092d9d5e3895d80b209c28..29665e00b4df973c34c87e80fc75cdd1f9c2b074 100644 (file)
     <localInspection language="Python" shortName="PyAbstractClassInspection" suppressId="PyAbstractClass" displayName="Class must implement all abstract methods" groupKey="INSP.GROUP.python" enabledByDefault="true"  level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyAbstractClassInspection"/>
     <localInspection language="Python" shortName="PyPep8NamingInspection" suppressId="PyPep8Naming" displayName="PEP 8 naming convention violation" groupKey="INSP.GROUP.python" enabledByDefault="true"  level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyPep8NamingInspection"/>
     <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"/>
 
     <defaultLiveTemplatesProvider implementation="com.jetbrains.python.codeInsight.liveTemplates.PyDefaultLiveTemplatesProvider"/>
diff --git a/python/src/com/jetbrains/python/inspections/PyDunderSlotsInspection.kt b/python/src/com/jetbrains/python/inspections/PyDunderSlotsInspection.kt
new file mode 100644 (file)
index 0000000..3eaad11
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2000-2016 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.jetbrains.python.PyNames
+import com.jetbrains.python.psi.*
+import com.jetbrains.python.psi.impl.PyPsiUtils
+
+class PyDunderSlotsInspection : 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 != null && LanguageLevel.forElement(node).isAtLeast(LanguageLevel.PYTHON30)) {
+        val slots = findSlotsValue(node)
+
+        when (slots) {
+          is PySequenceExpression -> slots
+              .elements
+              .asSequence()
+              .filterIsInstance<PyStringLiteralExpression>()
+              .forEach {
+                processSlot(node, it)
+              }
+          is PyStringLiteralExpression -> processSlot(node, slots)
+        }
+      }
+    }
+
+    private fun findSlotsValue(pyClass: PyClass): PyExpression? {
+      val target = pyClass.findClassAttribute(PyNames.SLOTS, false, myTypeEvalContext) as? PyTargetExpression
+      val value = target?.findAssignedValue()
+
+      return PyPsiUtils.flattenParens(value)
+    }
+
+    private fun processSlot(pyClass: PyClass, slot: PyStringLiteralExpression) {
+      val name = slot.stringValue
+
+      if (pyClass.findClassAttribute(name, false, myTypeEvalContext) != null) {
+        registerProblem(slot, "'$name' in __slots__ conflicts with class variable")
+      }
+    }
+  }
+}
+
diff --git a/python/testData/inspections/PyDunderSlotsInspection/test.py b/python/testData/inspections/PyDunderSlotsInspection/test.py
new file mode 100644 (file)
index 0000000..25e8864
--- /dev/null
@@ -0,0 +1,62 @@
+# PY-20280: one slot in list
+class Foo(object):
+    __slots__ = [<warning descr="'foo' in __slots__ conflicts with class variable">'foo'</warning>]
+    foo = 1
+
+
+# PY-20280: one slot in tuple
+class Foo(object):
+    __slots__ = (<warning descr="'foo' in __slots__ conflicts with class variable">'foo'</warning>)
+    foo = 1
+
+
+# PY-20280: one slot
+class Foo(object):
+    __slots__ = <warning descr="'foo' in __slots__ conflicts with class variable">'foo'</warning>
+    foo = 1
+
+
+# PY-20280: two slots in list
+class Foo(object):
+    __slots__ = [<warning descr="'foo' in __slots__ conflicts with class variable">'foo'</warning>, 'bar']
+    foo = 1
+
+
+# PY-20280: two slots in tuple
+class Foo(object):
+    __slots__ = (<warning descr="'foo' in __slots__ conflicts with class variable">'foo'</warning>, 'bar')
+    foo = 1
+
+
+# PY-20280: slots in base and class variable in derived
+class Base(object):
+    __slots__ = 'foo'
+
+class Derived(Base):
+    foo = 1
+
+
+# PY-20280: class variable in base and slots in derived
+class Base(object):
+    foo = 1
+
+class Derived(Base):
+    __slots__ = 'foo'
+
+
+# PY-20280: slots in base and derived, class variable in derived
+class Base(object):
+    __slots__ = 'foo'
+
+class Derived(Base):
+    __slots__ = 'bar'
+    foo = 1
+
+
+# PY-20280: slots in base and derived, class variable in base
+class Base(object):
+    __slots__ = 'bar'
+    foo = 1
+
+class Derived(Base):
+    __slots__ = 'foo'
\ No newline at end of file
index 26a1791d472090b5bfbb248e2d1fd618043ae431..da69bbbdfa4dbc88f88e9c9a7093baa6dafb7fd2 100644 (file)
@@ -328,4 +328,8 @@ public class PythonInspectionsTest extends PyTestCase {
   public void testPyShadowingNamesInspection() {
     doHighlightingTest(PyShadowingNamesInspection.class);
   }
+
+  public void testPyDunderSlotsInspection() {
+    runWithLanguageLevel(LanguageLevel.PYTHON30, () -> doHighlightingTest(PyDunderSlotsInspection.class));
+  }
 }