diff --git a/python/resources/inspectionDescriptions/PyDunderSlotsInspection.html b/python/resources/inspectionDescriptions/PyDunderSlotsInspection.html new file mode 100644 index 000000000000..e742af8b3367 --- /dev/null +++ b/python/resources/inspectionDescriptions/PyDunderSlotsInspection.html @@ -0,0 +1,5 @@ + + +This inspection detects invalid definition of __slots__ in a class. + + \ 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 f1415df9cf21..29665e00b4df 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -383,7 +383,7 @@ - + diff --git a/python/src/com/jetbrains/python/inspections/PyDunderSlotsInspection.kt b/python/src/com/jetbrains/python/inspections/PyDunderSlotsInspection.kt new file mode 100644 index 000000000000..3eaad119ed3b --- /dev/null +++ b/python/src/com/jetbrains/python/inspections/PyDunderSlotsInspection.kt @@ -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() + .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 index 000000000000..25e8864f649d --- /dev/null +++ b/python/testData/inspections/PyDunderSlotsInspection/test.py @@ -0,0 +1,62 @@ +# PY-20280: one slot in list +class Foo(object): + __slots__ = ['foo'] + foo = 1 + + +# PY-20280: one slot in tuple +class Foo(object): + __slots__ = ('foo') + foo = 1 + + +# PY-20280: one slot +class Foo(object): + __slots__ = 'foo' + foo = 1 + + +# PY-20280: two slots in list +class Foo(object): + __slots__ = ['foo', 'bar'] + foo = 1 + + +# PY-20280: two slots in tuple +class Foo(object): + __slots__ = ('foo', '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 diff --git a/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java b/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java index 26a1791d4720..da69bbbdfa4d 100644 --- a/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java +++ b/python/testSrc/com/jetbrains/python/PythonInspectionsTest.java @@ -328,4 +328,8 @@ public class PythonInspectionsTest extends PyTestCase { public void testPyShadowingNamesInspection() { doHighlightingTest(PyShadowingNamesInspection.class); } + + public void testPyDunderSlotsInspection() { + runWithLanguageLevel(LanguageLevel.PYTHON30, () -> doHighlightingTest(PyDunderSlotsInspection.class)); + } }