PY-34498 Add an inspection for pytest fixture that is not passed to test parameters

Report warning if a fixture is used without being passed to test function parameters or to
`@pytest.mark.usefixtures` decorator.


Co-authored-by: Denis Mashutin <Denis.Mashutin@jetbrains.com>


Merge-request: IJ-MR-108713
Merged-by: Egor Eliseev <Egor.Eliseev@jetbrains.com>

GitOrigin-RevId: 28d0711b99ab7ae180f672306dd4ab8a81f1feec
This commit is contained in:
Egor.Eliseev
2023-09-04 09:53:07 +00:00
committed by intellij-monorepo-bot
co-authored by Denis Mashutin
parent 42557af56d
commit 540f24faa4
23 changed files with 336 additions and 9 deletions
@@ -0,0 +1,5 @@
<html>
<body>
<p>Reports if a fixture is used without being passed to test function parameters or to <code>@pytest.mark.usefixtures</code> decorator</p>
</body>
</html>
@@ -546,6 +546,11 @@ QFIX.NAME.install.and.import.package=Install and import package ''{0}''
# PyAsyncCallInspection
QFIX.coroutine.is.not.awaited=Coroutine is not awaited
# PyTestUnpassedFixtureInspection
INSP.NAME.pytest.unpassed.fixture=Fixture is not requested by test functions
INSP.use.fixture.without.declaration.in.test.function=Fixture ''{0}'' is not requested by test functions or '@pytest.mark.usefixtures' marker
QFIX.add.fixture.to.test.function.parameters.list=Add fixture to test function parameters
# Actions and associated commands
ACT.CMD.use.import=Use an imported module
@@ -180,6 +180,10 @@
bundle="messages.PyPsiBundle" key="INSP.NAME.pytest-parametrized" groupKey="INSP.GROUP.python"
enabledByDefault="true" level="WARNING"
implementationClass="com.jetbrains.python.testing.pyTestParametrized.PyTestParametrizedInspection"/>
<localInspection language="Python" shortName="PyTestUnpassedFixtureInspection" suppressId="PyTestUnpassedFixture"
bundle="messages.PyPsiBundle" key="INSP.NAME.pytest.unpassed.fixture" groupKey="INSP.GROUP.python"
enabledByDefault="true" level="WARNING"
implementationClass="com.jetbrains.python.testing.pyTestFixtures.PyTestUnpassedFixtureInspection"/>
<localInspection shortName="PyPackageRequirementsInspection" suppressId="PyPackageRequirements" bundle="messages.PyPsiBundle"
key="INSP.NAME.requirements" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING"
implementationClass="com.jetbrains.python.inspections.PyPackageRequirementsInspection"/>
@@ -30,22 +30,32 @@ private fun PyDecoratorList.hasDecorator(vararg names: String) = names.any { fin
private fun PyElement.getFixtureName() = name ?: (this as? PyStringLiteralExpression)?.stringValue
/**
* Needed to avoid using the too wide `PyExpression` type
*/
private sealed class PyParameterOrPyReferenceExpression(val expression: PyExpression) {
class Parameter(expression: PyExpression) : PyParameterOrPyReferenceExpression(expression)
class Reference(expression: PyExpression) : PyParameterOrPyReferenceExpression(expression)
}
internal fun getFixtureLink(element: PyElement, typeEvalContext: TypeEvalContext): NamedFixtureLink? {
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return null
return when (element) {
is PyNamedParameter -> getFixtureAsParameterLink(element, typeEvalContext, module)
is PyNamedParameter -> getFixtureAsExpressionLink(PyParameterOrPyReferenceExpression.Parameter(element), typeEvalContext, module)
is PyReferenceExpression -> getFixtureAsExpressionLink(PyParameterOrPyReferenceExpression.Reference(element), typeEvalContext, module)
is PyStringLiteralExpression -> getFixtureAsStringLink(element, typeEvalContext, module)
else -> null
}
}
/**
* If named parameter has fixture (and import statement) -- return it
* If named parameter or reference expression has fixture (and import statement) -- return it
*/
private fun getFixtureAsParameterLink(element: PyNamedParameter, typeEvalContext: TypeEvalContext, module: Module): NamedFixtureLink? {
val func = PsiTreeUtil.getParentOfType(element, PyFunction::class.java) ?: return null
val fixtureCandidates = getFixtures(module, func, typeEvalContext).filter { o -> o.name == element.name }
return module.basePath?.let { return findRightFixture(fixtureCandidates, func, element, typeEvalContext, it) }
private fun getFixtureAsExpressionLink(element: PyParameterOrPyReferenceExpression, typeEvalContext: TypeEvalContext, module: Module): NamedFixtureLink? {
val pyExpression = element.expression
val func = PsiTreeUtil.getParentOfType(pyExpression, PyFunction::class.java) ?: return null
val fixtureCandidates = getFixtures(module, func, typeEvalContext).filter { o -> o.name == pyExpression.name }
return module.basePath?.let { return findRightFixture(fixtureCandidates, func, pyExpression, typeEvalContext, it) }
}
/**
@@ -103,7 +113,8 @@ private fun findRightFixture(fixtureCandidates: List<PyTestFixture>,
if (!fixtureCandidates.isEmpty()) {
val containingClass = if (pyFixtureElement is PyStringLiteralExpression) {
PsiTreeUtil.getParentOfType<PyDecorator>(pyFixtureElement)?.target
} else {
}
else {
func
}?.containingClass
containingClass?.let { pyClass ->
@@ -143,7 +154,7 @@ private fun findRightFixture(fixtureCandidates: List<PyTestFixture>,
// search reserved fixture class in "_pytest" dir
if (elementName in reservedFixtureClassSet) {
return NamedFixtureLink(PyTestFixture(null, null, elementName), null)
return NamedFixtureLink(PyTestFixture(null, null, elementName), null)
}
return null
}
@@ -2,17 +2,24 @@
package com.jetbrains.python.testing.pyTestFixtures
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiReference
import com.jetbrains.python.inspections.PyInspectionExtension
import com.jetbrains.python.psi.PyElement
import com.jetbrains.python.psi.PyFunction
import com.jetbrains.python.psi.PyNamedParameter
import com.jetbrains.python.psi.types.TypeEvalContext
/**
* fixture-based parameters should be skipped by inspection
* Fixture-based parameters should be skipped by inspection.
* Fixture-based unresolved reference should be ignored by inspection.
* @see PyTestUnpassedFixtureInspection
*/
class PyTestFixtureInspectionExtension : PyInspectionExtension() {
override fun ignoreUnused(local: PsiElement, evalContext: TypeEvalContext) =
local is PyNamedParameter && local.isFixture(evalContext)
override fun ignoreShadowed(element: PsiElement) = element is PyFunction && element.isFixture()
override fun ignoreUnresolvedReference(node: PyElement, reference: PsiReference, context: TypeEvalContext) =
getFixtureLink(node, context) != null
}
@@ -0,0 +1,98 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.testing.pyTestFixtures
import com.intellij.codeInspection.*
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.util.findParentOfType
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.inspections.PyInspection
import com.jetbrains.python.inspections.PyInspectionVisitor
import com.jetbrains.python.psi.PyDecorator
import com.jetbrains.python.psi.PyElementGenerator
import com.jetbrains.python.psi.PyFunction
import com.jetbrains.python.psi.PyReferenceExpression
import com.jetbrains.python.psi.PyStringLiteralExpression
import com.jetbrains.python.psi.types.PyFunctionType
import com.jetbrains.python.psi.types.TypeEvalContext
/**
* Reports if a fixture is used without being passed to test function parameters.
* Suggests a quick fix "Add fixture to test function parameters"
*/
class PyTestUnpassedFixtureInspection : PyInspection() {
/**
* Describes an `element` type into which `element: PyReferenceExpression` resolves.
*
* Type is `PyFixture` -> `FIXTURE`
*
* Type is unresolved -> `NONE`
*
* else -> `OTHER`
*/
private enum class ResolveType {
FIXTURE,
NONE,
OTHER
}
private val quickFix = AddFixtureToFuncParametersQuickFix()
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
return Visitor(holder, PyInspectionVisitor.getContext(session))
}
private fun findTestFunc(element: PyReferenceExpression) = element.findParentOfType<PyFunction>()
private fun isDecorator(element: PyReferenceExpression) = element.findParentOfType<PyDecorator>() != null
private inner class Visitor(holder: ProblemsHolder, context: TypeEvalContext) : PyInspectionVisitor(holder, context) {
override fun visitPyReferenceExpression(element: PyReferenceExpression) {
if (isDecorator(element)) return
val testFunction = findTestFunc(element) ?: return
// no warning if a fixture is in test function parameters
testFunction.parameterList.parameters.find { it.name == element.name }?.let { return }
// no warning if a fixture is in '@pytest.mark.usefixtures' arguments
if (isParameterInDecorator(element, testFunction)) return
// no warning if an element has type OTHER
if (getType(element) == ResolveType.OTHER) return
// try to find a fixture from conftest.py file
getFixtureLink(element, myTypeEvalContext) ?: return
holder?.registerProblem(element, PyPsiBundle.message("INSP.use.fixture.without.declaration.in.test.function", element.text),
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
quickFix)
}
private fun getType(element: PyReferenceExpression): ResolveType {
val type = myTypeEvalContext.getType(element) ?: return ResolveType.NONE
val fixture = (type as? PyFunctionType)?.callable
if (fixture is PyFunction && fixture.isFixture()) {
return ResolveType.FIXTURE
}
return ResolveType.OTHER
}
private fun isParameterInDecorator(element: PyReferenceExpression, testFunction: PyFunction): Boolean {
val pytestUseFixturesDecorator = testFunction.decoratorList?.decorators?.find { it.name == USE_FIXTURES }
return pytestUseFixturesDecorator?.argumentList?.arguments?.find { it is PyStringLiteralExpression && it.stringValue == element.name } != null
}
}
private inner class AddFixtureToFuncParametersQuickFix : LocalQuickFix {
override fun getFamilyName() = PyPsiBundle.message("QFIX.add.fixture.to.test.function.parameters.list")
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
val element = descriptor.psiElement as? PyReferenceExpression ?: return
val newParameter = PyElementGenerator.getInstance(project).createParameter(element.text)
findTestFunc(element)?.parameterList?.addParameter(newParameter)
}
}
}
@@ -0,0 +1,11 @@
import pytest
@pytest.fixture
def foo():
return 1
@pytest.mark.usefixtures("foo")
def test_():
assert foo == 1
@@ -0,0 +1,10 @@
import pytest
@pytest.fixture
def foo():
return 1
def test_(foo):
assert foo == 1
@@ -0,0 +1,7 @@
import pytest
foo = 1
def test_():
assert foo == 1
@@ -0,0 +1,6 @@
import pytest
from ResolvedExpression import foo
def test_():
assert foo == 1
@@ -0,0 +1,10 @@
import pytest
@pytest.fixture
def foo():
return 1
def test_():
assert <warning descr="Fixture 'foo' is not requested by test functions or @pytest.mark.usefixtures marker">foo</warning> == 1
@@ -0,0 +1,5 @@
import pytest
def test_():
assert <warning descr="Fixture 'bar' is not requested by test functions or @pytest.mark.usefixtures marker">bar</warning> == 1
@@ -0,0 +1,5 @@
import pytest
from FixtureInDecorator import foo
def test_():
assert <warning descr="Fixture 'foo' is not requested by test functions or @pytest.mark.usefixtures marker">foo</warning> == 1
@@ -0,0 +1,6 @@
import pytest
@pytest.fixture
def bar():
return 1
@@ -0,0 +1,6 @@
import pytest
@pytest.fixture
def bar():
return 1
@@ -0,0 +1,10 @@
import pytest
@pytest.fixture
def foo():
return 1
def test_():
assert <warning descr="Fixture 'foo' is not requested by test functions or @pytest.mark.usefixtures marker"><caret>foo</warning> == 1
@@ -0,0 +1,10 @@
import pytest
@pytest.fixture
def foo():
return 1
def test_(foo):
assert foo == 1
@@ -0,0 +1,5 @@
import pytest
def test_():
assert <warning descr="Fixture 'bar' is not requested by test functions or @pytest.mark.usefixtures marker"><caret>bar</warning> == 1
@@ -0,0 +1,5 @@
import pytest
def test_(bar):
assert bar == 1
@@ -0,0 +1,5 @@
import pytest
from simpleUnpassedFixture import foo
def test_():
assert <warning descr="Fixture 'foo' is not requested by test functions or @pytest.mark.usefixtures marker"><caret>foo</warning> == 1
@@ -0,0 +1,5 @@
import pytest
from simpleUnpassedFixture import foo
def test_(foo):
assert foo == 1
@@ -0,0 +1,37 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.quickFixes
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.PyQuickFixTestCase
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.testing.PythonTestConfigurationType
import com.jetbrains.python.testing.TestRunnerService
import com.jetbrains.python.testing.pyTestFixtures.PyTestUnpassedFixtureInspection
class PyTestAddFixtureToFuncParameterQuickFixTest : PyQuickFixTestCase() {
private fun doTest() {
doQuickFixTest(PyTestUnpassedFixtureInspection::class.java,
PyPsiBundle.message("QFIX.add.fixture.to.test.function.parameters.list"),
LanguageLevel.getLatest())
}
override fun setUp() {
super.setUp()
TestRunnerService.getInstance(myFixture.module).selectedFactory =
PythonTestConfigurationType.getInstance().pyTestFactory
myFixture.copyDirectoryToProject("", "")
}
fun testSimpleUnpassedFixture() {
doTest()
}
fun testUnpassedFixtureFromImport() {
doTest()
}
fun testUnpassedFixtureFromConftest() {
doTest()
}
}
@@ -0,0 +1,54 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.testing
import com.intellij.testFramework.LightProjectDescriptor
import com.jetbrains.python.fixtures.PyInspectionTestCase
import com.jetbrains.python.inspections.PyInspection
import com.jetbrains.python.testing.pyTestFixtures.PyTestUnpassedFixtureInspection
class PyTestUnpassedFixtureInspectionTest : PyInspectionTestCase() {
override fun getInspectionClass(): Class<out PyInspection> {
return PyTestUnpassedFixtureInspection::class.java
}
override fun setUp() {
super.setUp()
TestRunnerService.getInstance(myFixture.module).selectedFactory =
PythonTestConfigurationType.getInstance().pyTestFactory
myFixture.copyDirectoryToProject("", "")
}
override fun getProjectDescriptor(): LightProjectDescriptor? {
return ourPyLatestDescriptor
}
override fun isLowerCaseTestFile(): Boolean = false
fun testFixtureInDecorator() {
doTest()
}
fun testFixtureInParameters() {
doTest()
}
fun testResolvedExpression() {
doTest()
}
fun testResolvedExpressionFromImport() {
doTest()
}
fun testSimpleUnpassedFixture() {
doTest()
}
fun testUnpassedFixtureFromConftest() {
doTest()
}
fun testUnpassedFixtureFromImport() {
doTest()
}
}