PY-9687 PyUnusedLocalInspection: split into three separate inspections

(cherry picked from commit 6544ea37b7b8db58f8555b9b7c53d7922158f125)

GitOrigin-RevId: ae66cceb9c9b6465a8a127937ae736823580313f
This commit is contained in:
Morgan Bartholomew
2026-07-08 03:10:59 +00:00
committed by intellij-monorepo-bot
parent dfa8ca325e
commit 1b2d29fa8c
23 changed files with 477 additions and 31 deletions
@@ -0,0 +1,7 @@
<html>
<body>
<p>
Reports local functions that are defined but never used.
</p>
</body>
</html>
@@ -1,7 +1,7 @@
<html>
<body>
<p>
Reports local variables, parameters, and functions that are locally defined, but not used name in a function.
Reports local variables and other local symbols that are locally defined, but not used name in a function.
</p>
</body>
</html>
@@ -0,0 +1,7 @@
<html>
<body>
<p>
Reports parameters of a function that are not used in its body.
</p>
</body>
</html>
@@ -236,6 +236,14 @@
"id": "PyUnusedLocalInspection",
"codeQualityCategory": "Code Style"
},
{
"id": "PyUnusedParameterInspection",
"codeQualityCategory": "Code Style"
},
{
"id": "PyUnusedFunctionInspection",
"codeQualityCategory": "Code Style"
},
{
"id": "PyMethodFirstArgAssignmentInspection",
"codeQualityCategory": "Reliability"
@@ -199,6 +199,10 @@
<statistics.applicationUsagesCollector implementation="com.jetbrains.python.psi.types.PyTypeEvaluationAggregatesCollector"/>
<localInspection language="Python" shortName="PyUnusedLocalInspection" suppressId="PyUnusedLocal" bundle="messages.PyPsiBundle" key="INSP.NAME.unused" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.unusedLocal.PyUnusedLocalInspection"/>
<localInspection language="Python" shortName="PyUnusedParameterInspection" suppressId="PyUnusedParameter" bundle="messages.PyPsiBundle" key="INSP.NAME.unused.parameter" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.unusedLocal.PyUnusedParameterInspection"/>
<localInspection language="Python" shortName="PyUnusedFunctionInspection" suppressId="PyUnusedFunction" bundle="messages.PyPsiBundle" key="INSP.NAME.unused.function" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.unusedLocal.PyUnusedFunctionInspection"/>
<inspectionElementsMerger implementation="com.jetbrains.python.inspections.unusedLocal.PyUnusedParameterInspectionMerger"/>
<inspectionElementsMerger implementation="com.jetbrains.python.inspections.unusedLocal.PyUnusedFunctionInspectionMerger"/>
<localInspection language="Python" shortName="PyRedundantParenthesesInspection" suppressId="PyRedundantParentheses" bundle="messages.PyPsiBundle" key="INSP.NAME.redundant.parentheses" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PyRedundantParenthesesInspection"/>
<localInspection language="Python" shortName="PySimplifyBooleanCheckInspection" suppressId="PySimplifyBooleanCheck" bundle="messages.PyPsiBundle" key="INSP.NAME.check.can.be.simplified" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WEAK WARNING" implementationClass="com.jetbrains.python.inspections.PySimplifyBooleanCheckInspection"/>
<localInspection language="Python" shortName="PyDefaultArgumentInspection" suppressId="PyDefaultArgument" bundle="messages.PyPsiBundle" key="INSP.NAME.default.argument" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyDefaultArgumentInspection"/>
@@ -880,6 +880,8 @@ INSP.mock.patch.too.many.params=Function has {0} extra parameter(s) not matched
# PyUnusedLocalInspection
INSP.NAME.unused=Unused local symbols
INSP.NAME.unused.parameter=Unused parameter
INSP.NAME.unused.function=Unused function
INSP.unused.locals.parameter.isnot.used=Parameter `{0}` value is not used
INSP.unused.locals.local.variable.isnot.used=Local variable `{0}` value is not used
INSP.unused.locals.replace.with.wildcard=Replace with _
@@ -0,0 +1,11 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections.unusedLocal
/**
* Reports unused local functions. Unused local variables and other local symbols are reported separately by
* [PyUnusedLocalInspection]; both inspections share [PyUnusedLocalInspectionVisitor] for the analysis.
*/
class PyUnusedFunctionInspection : PyUnusedSymbolInspection() {
override val reportTarget: PyUnusedLocalInspectionVisitor.ReportTarget
get() = PyUnusedLocalInspectionVisitor.ReportTarget.FUNCTIONS
}
@@ -19,7 +19,6 @@ public final class PyUnusedLocalInspection extends PyInspection {
private static final Key<PyUnusedLocalInspectionVisitor> KEY = Key.create("PyUnusedLocal.Visitor");
public boolean ignoreTupleUnpacking = true;
public boolean ignoreLambdaParameters = true;
public boolean ignoreLoopIterationVariables = true;
public boolean ignoreVariablesStartingWithUnderscore = true;
@@ -29,9 +28,11 @@ public final class PyUnusedLocalInspection extends PyInspection {
@NotNull LocalInspectionToolSession session) {
final PyUnusedLocalInspectionVisitor visitor = new PyUnusedLocalInspectionVisitor(holder,
ignoreTupleUnpacking,
ignoreLambdaParameters,
true,
ignoreLoopIterationVariables,
ignoreVariablesStartingWithUnderscore,
PyUnusedLocalInspectionVisitor.ReportTarget.LOCALS,
PyUnusedLocalInspectionVisitor.getSharedAnalysis(session),
PyInspectionVisitor.getContext(session));
// buildVisitor() will be called on injected files in the same session - don't overwrite if we already have one
final PyUnusedLocalInspectionVisitor existingVisitor = session.getUserData(KEY);
@@ -54,7 +55,6 @@ public final class PyUnusedLocalInspection extends PyInspection {
public @NotNull OptPane getOptionsPane() {
return pane(
checkbox("ignoreTupleUnpacking", PyPsiBundle.message("INSP.unused.locals.ignore.variables.used.in.tuple.unpacking")),
checkbox("ignoreLambdaParameters", PyPsiBundle.message("INSP.unused.locals.ignore.lambda.parameters")),
checkbox("ignoreLoopIterationVariables", PyPsiBundle.message("INSP.unused.locals.ignore.range.iteration.variables")),
checkbox("ignoreVariablesStartingWithUnderscore", PyPsiBundle.message("INSP.unused.locals.ignore.variables.starting.with"))
);
@@ -3,6 +3,7 @@ package com.jetbrains.python.inspections.unusedLocal;
import com.intellij.codeInsight.controlflow.ControlFlowUtil;
import com.intellij.codeInsight.controlflow.Instruction;
import com.intellij.codeInspection.LocalInspectionToolSession;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
@@ -10,6 +11,7 @@ import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
@@ -102,23 +104,64 @@ public final class PyUnusedLocalInspectionVisitor extends PyInspectionVisitor {
private final boolean myIgnoreLambdaParameters;
private final boolean myIgnoreRangeIterationVariables;
private final boolean myIgnoreVariablesStartingWithUnderscore;
private final ReportTarget myReportTarget;
private final SharedAnalysis mySharedAnalysis;
/** Which kind of unused element this visitor reports; each kind is exposed as a separate inspection. */
public enum ReportTarget { LOCALS, PARAMETERS, FUNCTIONS }
/**
* Analysis state shared by the unused-locals, unused-parameter, and unused-function inspections. All three perform the same
* control-flow analysis but report different element kinds; sharing this state through the {@link LocalInspectionToolSession}
* keeps the heavy analysis running once per file instead of once per inspection.
*/
public static final class SharedAnalysis {
// Names defined directly in a scope. They all belong to this scope.
private final Map<ScopeOwner, @Unmodifiable Set<PsiElement>> scopeWrites = new ConcurrentHashMap<>();
// Names read directly in a scope. They might belong to some outer scope.
private final Map<ScopeOwner, @Unmodifiable Set<PsiElement>> scopeReads = new ConcurrentHashMap<>();
private volatile Set<PsiElement> unusedElements;
}
private static final Key<SharedAnalysis> SHARED_ANALYSIS_KEY = Key.create("PyUnusedLocal.SharedAnalysis");
/** Returns the analysis state shared by the unused-symbol inspections running on this session, creating it on first use. */
public static @NotNull SharedAnalysis getSharedAnalysis(@NotNull LocalInspectionToolSession session) {
SharedAnalysis analysis = session.getUserData(SHARED_ANALYSIS_KEY);
if (analysis == null) {
synchronized (SHARED_ANALYSIS_KEY) {
analysis = session.getUserData(SHARED_ANALYSIS_KEY);
if (analysis == null) {
analysis = new SharedAnalysis();
session.putUserData(SHARED_ANALYSIS_KEY, analysis);
}
}
}
return analysis;
}
// Names defined directly in a scope. They all belong to this scope.
private final Map<ScopeOwner, @Unmodifiable Set<PsiElement>> myScopeWrites = new ConcurrentHashMap<>();
private final Map<ScopeOwner, @Unmodifiable Set<PsiElement>> myScopeWrites;
// Names read directly in a scope. They might belong to some outer scope.
private final Map<ScopeOwner, @Unmodifiable Set<PsiElement>> myScopeReads = new ConcurrentHashMap<>();
private final Map<ScopeOwner, @Unmodifiable Set<PsiElement>> myScopeReads;
public PyUnusedLocalInspectionVisitor(@NotNull ProblemsHolder holder,
boolean ignoreTupleUnpacking,
boolean ignoreLambdaParameters,
boolean ignoreRangeIterationVariables,
boolean ignoreVariablesStartingWithUnderscore,
@NotNull ReportTarget reportTarget,
@NotNull SharedAnalysis sharedAnalysis,
@NotNull TypeEvalContext context) {
super(holder, context);
myIgnoreTupleUnpacking = ignoreTupleUnpacking;
myIgnoreLambdaParameters = ignoreLambdaParameters;
myIgnoreRangeIterationVariables = ignoreRangeIterationVariables;
myIgnoreVariablesStartingWithUnderscore = ignoreVariablesStartingWithUnderscore;
myReportTarget = reportTarget;
mySharedAnalysis = sharedAnalysis;
myScopeWrites = sharedAnalysis.scopeWrites;
myScopeReads = sharedAnalysis.scopeReads;
}
@Override
@@ -150,6 +193,10 @@ public final class PyUnusedLocalInspectionVisitor extends PyInspectionVisitor {
}
private void collectAllWrites(ScopeOwner owner) {
if (myScopeWrites.containsKey(owner)) {
// Already computed for another unused-symbol inspection sharing this session.
return;
}
Set<PsiElement> scopeWrites = new HashSet<>();
// type parameter list is not included in CFG
if (owner instanceof PyTypeParameterListOwner typeParameterListOwner) {
@@ -379,6 +426,16 @@ public final class PyUnusedLocalInspectionVisitor extends PyInspectionVisitor {
return false;
}
private static @NotNull ReportTarget categorize(@NotNull PsiElement element) {
if (element instanceof PyNamedParameter || element.getParent() instanceof PyNamedParameter) {
return ReportTarget.PARAMETERS;
}
if (element instanceof PyFunction) {
return ReportTarget.FUNCTIONS;
}
return ReportTarget.LOCALS;
}
public void registerProblems() {
final List<PyInspectionExtension> filters = PyInspectionExtension.EP_NAME.getExtensionList();
// Register problems
@@ -386,11 +443,21 @@ public final class PyUnusedLocalInspectionVisitor extends PyInspectionVisitor {
final Set<PyFunction> functionsWithInheritors = new HashSet<>();
final Map<PyFunction, Boolean> emptyFunctions = new HashMap<>();
Set<PsiElement> unusedElements = StreamEx.of(myScopeWrites.entrySet())
.flatCollection(writeEntry -> ContainerUtil.subtract(writeEntry.getValue(), getReadsInsideScope(writeEntry.getKey())))
.toImmutableSet();
Set<PsiElement> unusedElements = mySharedAnalysis.unusedElements;
if (unusedElements == null) {
unusedElements = StreamEx.of(myScopeWrites.entrySet())
.flatCollection(writeEntry -> ContainerUtil.subtract(writeEntry.getValue(), getReadsInsideScope(writeEntry.getKey())))
.toImmutableSet();
// Benign race: concurrent inspections may recompute the identical set; the last write wins.
mySharedAnalysis.unusedElements = unusedElements;
}
for (PsiElement element : unusedElements) {
// Parameters, functions, and the remaining "local symbols" are each reported by a separate inspection.
if (categorize(element) != myReportTarget) {
continue;
}
boolean ignoreUnused = false;
for (PyInspectionExtension filter : filters) {
if (filter.ignoreUnused(element, myTypeEvalContext)) {
@@ -0,0 +1,24 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections.unusedLocal
import com.intellij.codeInspection.options.OptPane
import com.intellij.codeInspection.options.OptPane.pane
import com.jetbrains.python.PyPsiBundle
/**
* Reports unused function parameters. Unused local variables and other local symbols are reported separately by
* [PyUnusedLocalInspection]; both inspections share [PyUnusedLocalInspectionVisitor] for the analysis.
*/
class PyUnusedParameterInspection : PyUnusedSymbolInspection() {
@JvmField
var ignoreLambdaParameters: Boolean = true
override val reportTarget: PyUnusedLocalInspectionVisitor.ReportTarget
get() = PyUnusedLocalInspectionVisitor.ReportTarget.PARAMETERS
override fun shouldIgnoreLambdaParameters(): Boolean = ignoreLambdaParameters
override fun getOptionsPane(): OptPane = pane(
OptPane.checkbox("ignoreLambdaParameters", PyPsiBundle.message("INSP.unused.locals.ignore.lambda.parameters")),
)
}
@@ -0,0 +1,44 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections.unusedLocal
import com.intellij.codeInspection.LocalInspectionToolSession
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.openapi.application.runReadActionBlocking
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiElementVisitor
import com.jetbrains.python.inspections.PyInspection
import com.jetbrains.python.inspections.PyInspectionVisitor
/**
* Base class for the unused-parameter and unused-function inspections. Both report a single kind of unused symbol via the shared
* [PyUnusedLocalInspectionVisitor]; the actual control-flow analysis is shared across all unused-symbol inspections running on the
* same session (see [PyUnusedLocalInspectionVisitor.SharedAnalysis]). Unused local variables and other local symbols are reported
* separately by [PyUnusedLocalInspection].
*/
abstract class PyUnusedSymbolInspection : PyInspection() {
private val visitorKey = Key.create<PyUnusedLocalInspectionVisitor>("${javaClass.simpleName}.Visitor")
/** The single element kind this inspection reports. */
protected abstract val reportTarget: PyUnusedLocalInspectionVisitor.ReportTarget
/** Whether lambda parameters should be ignored; only meaningful when [reportTarget] is `PARAMETERS`. */
protected open fun shouldIgnoreLambdaParameters(): Boolean = true
final override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
val visitor = PyUnusedLocalInspectionVisitor(holder, false, shouldIgnoreLambdaParameters(), false, false,
reportTarget,
PyUnusedLocalInspectionVisitor.getSharedAnalysis(session),
PyInspectionVisitor.getContext(session))
// buildVisitor() will be called on injected files in the same session - don't overwrite if we already have one
if (session.getUserData(visitorKey) == null) {
session.putUserData(visitorKey, visitor)
}
return visitor
}
final override fun inspectionFinished(session: LocalInspectionToolSession, holder: ProblemsHolder) {
val visitor = session.getUserData(visitorKey) ?: return
runReadActionBlocking { visitor.registerProblems() }
session.putUserData(visitorKey, null)
}
}
@@ -0,0 +1,59 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections.unusedLocal
import com.intellij.codeInspection.ex.InspectionElementsMergerBase
import org.jdom.Element
/**
* Migrates user inspection-profile settings (enabled state, severity, and shared options such as `ignoreLambdaParameters`) from
* the former combined `PyUnusedLocalInspection` to the inspection that took over part of its reporting after PY-9687 split it into
* separate unused-local / unused-parameter / unused-function inspections. Without this, a user who had disabled or reconfigured
* `PyUnusedLocalInspection` would silently get the new inspections back at their defaults.
*
* Suppression is intentionally NOT inherited: `# noinspection PyUnusedLocal` no longer silences parameters or functions (the split
* is a deliberate clean break), so [getSuppressIds] returns only this inspection's own id instead of letting the base fall back to
* the source tool name.
*/
abstract class PyUnusedSymbolInspectionMerger : InspectionElementsMergerBase() {
/** Suppress id of the merged (new) inspection. */
protected abstract val mergedSuppressId: String
final override fun getSourceToolNames(): Array<String> = arrayOf(SOURCE_TOOL_NAME)
final override fun getSuppressIds(): Array<String> = arrayOf(mergedSuppressId)
final override fun merge(inspectionElements: Map<String, Element>): Element? {
val merged = super.merge(inspectionElements)
if (merged != null) return merged
// super.merge() produces nothing when the source node carried no option children (e.g. a plain enable/disable toggle or a
// severity change). Still carry over the enabled state and severity so the split stays transparent to such users.
val source = inspectionElements[SOURCE_TOOL_NAME] ?: return null
return Element(INSPECTION_TOOL_TAG).apply {
setAttribute(CLASS_ATTR, mergedToolName)
source.getAttributeValue(ENABLED_ATTR)?.let { setAttribute(ENABLED_ATTR, it) }
source.getAttributeValue(ENABLED_BY_DEFAULT_ATTR)?.let { setAttribute(ENABLED_BY_DEFAULT_ATTR, it) }
source.getAttributeValue(LEVEL_ATTR)?.let { setAttribute(LEVEL_ATTR, it) }
}
}
private companion object {
const val SOURCE_TOOL_NAME = "PyUnusedLocalInspection"
// Inspection-profile serialization names (mirror InspectionProfileImpl/ToolsImpl, which are not visible from here).
const val INSPECTION_TOOL_TAG = "inspection_tool"
const val CLASS_ATTR = "class"
const val ENABLED_ATTR = "enabled"
const val ENABLED_BY_DEFAULT_ATTR = "enabled_by_default"
const val LEVEL_ATTR = "level"
}
}
class PyUnusedParameterInspectionMerger : PyUnusedSymbolInspectionMerger() {
override fun getMergedToolName(): String = "PyUnusedParameterInspection"
override val mergedSuppressId: String get() = "unused-parameter"
}
class PyUnusedFunctionInspectionMerger : PyUnusedSymbolInspectionMerger() {
override fun getMergedToolName(): String = "PyUnusedFunctionInspection"
override val mergedSuppressId: String get() = "unused-function"
}
@@ -10,7 +10,9 @@ import com.jetbrains.python.inspections.PyPep8Inspection;
import com.jetbrains.python.inspections.PySingleQuotedDocstringInspection;
import com.jetbrains.python.inspections.PyStatementEffectInspection;
import com.jetbrains.python.inspections.PyUnboundLocalVariableInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedFunctionInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedLocalInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedParameterInspection;
import com.jetbrains.python.psi.PythonVisitorFilter;
import com.jetbrains.python.validation.PyDocStringHighlightingAnnotator;
import org.jetbrains.annotations.NotNull;
@@ -26,7 +28,8 @@ final class ConsoleVisitorFilter implements PythonVisitorFilter {
//if we're in console
if (PydevConsoleRunnerUtil.isInPydevConsole(file)) {
//inspections
if (visitorClass == PyUnusedLocalInspection.class || visitorClass == PyUnboundLocalVariableInspection.class ||
if (visitorClass == PyUnusedLocalInspection.class || visitorClass == PyUnusedParameterInspection.class ||
visitorClass == PyUnusedFunctionInspection.class || visitorClass == PyUnboundLocalVariableInspection.class ||
visitorClass == PyStatementEffectInspection.class || visitorClass == PySingleQuotedDocstringInspection.class ||
visitorClass == PyIncorrectDocstringInspection.class || visitorClass == PyMissingOrEmptyDocstringInspection.class ||
visitorClass == PyMandatoryEncodingInspection.class || visitorClass == PyPep8Inspection.class ||
@@ -10,7 +10,9 @@ import com.jetbrains.python.inspections.PyPropertyDefinitionInspection;
import com.jetbrains.python.inspections.PyShadowingBuiltinsInspection;
import com.jetbrains.python.inspections.PyStatementEffectInspection;
import com.jetbrains.python.inspections.PyTypeCheckerInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedFunctionInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedLocalInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedParameterInspection;
import com.jetbrains.python.psi.PythonVisitorFilter;
import org.jetbrains.annotations.NotNull;
@@ -20,6 +22,8 @@ public final class PyiVisitorFilter implements PythonVisitorFilter {
private static final @NotNull Set<Class<?>> disabledVisitors = ImmutableSet.of(
PyUnusedLocalInspection.class,
PyUnusedParameterInspection.class,
PyUnusedFunctionInspection.class,
PyStatementEffectInspection.class,
PyCompatibilityInspection.class,
PyMissingOrEmptyDocstringInspection.class,
@@ -0,0 +1,59 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.idea.TestFor
import com.jetbrains.python.PythonFileType
import com.jetbrains.python.fixtures.PyCodeInsightTestCase
import com.jetbrains.python.inspections.unusedLocal.PyUnusedFunctionInspection
import org.intellij.lang.annotations.Language
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
@TestFor(issues = ["PY-9687"])
class PyUnusedFunctionInspectionTest : PyCodeInsightTestCase() {
/** Asserts the exact set of unused-function warnings (descriptions) reported for [code]. */
private fun doTest(@Language("Python") code: String, vararg expectedMessages: String) {
myFixture.configureByText(PythonFileType.INSTANCE, code.trimIndent())
myFixture.enableInspections(PyUnusedFunctionInspection())
val actual = myFixture.doHighlighting()
.filter { it.severity == HighlightSeverity.WEAK_WARNING }
.map { it.description }
.sorted()
Assertions.assertEquals(expectedMessages.sorted(), actual)
}
@Test
fun `unused local function is reported`() {
// The unused local variable 'a' belongs to PyUnusedLocal and is not enabled here.
doTest("""
def outer():
a = 1
def inner():
return 1
return a
""", "Local function 'inner' is not used")
}
@Test
fun `used local function is not reported`() {
doTest("""
def outer():
def inner():
return 1
return inner()
""")
}
@Test
fun `noinspection unused-function suppresses the function`() {
doTest("""
def outer():
# noinspection unused-function
def inner():
return 1
return 0
""")
}
}
@@ -15,19 +15,25 @@
*/
package com.jetbrains.python.inspections;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.idea.TestFor;
import com.jetbrains.python.fixtures.PyInspectionTestCase;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedFunctionInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedLocalInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedParameterInspection;
import com.jetbrains.python.psi.LanguageLevel;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class PyUnusedLocalInspectionTest extends PyInspectionTestCase {
public void testPy2() {
final PyUnusedLocalInspection inspection = new PyUnusedLocalInspection();
inspection.ignoreTupleUnpacking = false;
inspection.ignoreLambdaParameters = false;
runWithLanguageLevel(LanguageLevel.PYTHON27, () -> doTest(inspection));
final PyUnusedParameterInspection parameterInspection = new PyUnusedParameterInspection();
parameterInspection.ignoreLambdaParameters = false;
runWithLanguageLevel(LanguageLevel.PYTHON27, () -> doTest(inspection, parameterInspection));
}
public void testNonlocal() {
@@ -149,7 +155,6 @@ public class PyUnusedLocalInspectionTest extends PyInspectionTestCase {
public void testIgnoringVariablesStartingWithUnderscore() {
final PyUnusedLocalInspection inspection = new PyUnusedLocalInspection();
inspection.ignoreVariablesStartingWithUnderscore = true;
inspection.ignoreLambdaParameters = false;
inspection.ignoreLoopIterationVariables = false;
inspection.ignoreTupleUnpacking = false;
doTest(inspection);
@@ -310,10 +315,21 @@ def test():
return PyUnusedLocalInspection.class;
}
// Parameter and function reporting now live in separate inspections; enable them too so the shared testData keeps matching.
@NotNull
@Override
protected List<Class<? extends LocalInspectionTool>> getAdditionalInspectionClasses() {
return List.of(PyUnusedParameterInspection.class, PyUnusedFunctionInspection.class);
}
private void doTest(@NotNull PyUnusedLocalInspection inspection) {
doTest(inspection, new PyUnusedParameterInspection());
}
private void doTest(@NotNull PyUnusedLocalInspection inspection, @NotNull PyUnusedParameterInspection parameterInspection) {
final String path = "inspections/PyUnusedLocalInspection/" + getTestName(true) + ".py";
myFixture.configureByFile(path);
myFixture.enableInspections(inspection);
myFixture.enableInspections(inspection, parameterInspection, new PyUnusedFunctionInspection());
myFixture.checkHighlighting(true, false, true);
}
}
@@ -0,0 +1,71 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.idea.TestFor
import com.jetbrains.python.PythonFileType
import com.jetbrains.python.fixtures.PyCodeInsightTestCase
import com.jetbrains.python.inspections.unusedLocal.PyUnusedParameterInspection
import org.intellij.lang.annotations.Language
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
@TestFor(issues = ["PY-9687"])
class PyUnusedParameterInspectionTest : PyCodeInsightTestCase() {
/** Asserts the exact set of unused-parameter warnings (descriptions) reported for [code]. */
private fun doTest(@Language("Python") code: String, vararg expectedMessages: String) {
myFixture.configureByText(PythonFileType.INSTANCE, code.trimIndent())
myFixture.enableInspections(PyUnusedParameterInspection())
val actual = myFixture.doHighlighting()
.filter { it.severity == HighlightSeverity.WEAK_WARNING }
.map { it.description }
.sorted()
Assertions.assertEquals(expectedMessages.sorted(), actual)
}
@Test
fun `unused parameter is reported`() {
// The unused local 'a' belongs to PyUnusedLocal and is not enabled here.
doTest("""
def f(x):
a = 1
""", "Parameter 'x' value is not used")
}
@Test
fun `self is not reported but a regular parameter is`() {
doTest("""
class A:
def m(self, x):
return 1
""", "Parameter 'x' value is not used")
}
@Test
fun `lambda parameter is not reported`() {
doTest("""
f = lambda x: 1
print(f)
""")
}
@Test
fun `noinspection unused-parameter suppresses the parameter`() {
doTest("""
# noinspection unused-parameter
def f(x):
return 1
""")
}
@Test
fun `legacy PyUnusedLocal does not suppress the parameter`() {
// Clean break: PyUnusedLocal no longer silences parameters.
doTest("""
# noinspection PyUnusedLocal
def f(x):
return 1
""", "Parameter 'x' value is not used")
}
}
@@ -0,0 +1,57 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections
import com.intellij.codeInspection.ex.InspectionProfileImpl
import com.intellij.codeInspection.ex.InspectionToolRegistrar
import com.intellij.idea.TestFor
import com.intellij.openapi.util.JDOMUtil
import com.jetbrains.python.fixtures.PyTestCase
import com.jetbrains.python.inspections.unusedLocal.PyUnusedParameterInspection
/**
* Verifies that profile settings from the former combined `PyUnusedLocalInspection` migrate to the inspections that PY-9687 split
* it into, so users who had disabled or reconfigured it are not silently reset to defaults.
*/
@TestFor(issues = ["PY-9687"])
class PyUnusedSymbolInspectionMergerTest : PyTestCase() {
private lateinit var profile: InspectionProfileImpl
override fun setUp() {
super.setUp()
InspectionProfileImpl.INIT_INSPECTIONS = true
profile = InspectionProfileImpl("Test", InspectionToolRegistrar.getInstance(), InspectionProfileImpl("base"))
}
override fun tearDown() {
try {
InspectionProfileImpl.INIT_INSPECTIONS = false
}
finally {
super.tearDown()
}
}
// A user who disabled the former combined inspection should not get the split-out inspections back, enabled, at their defaults.
fun testDisabledStateMigratesToSplitInspections() {
profile.readExternal(JDOMUtil.load("""
<profile version="1.0">
<option name="myName" value="Test" />
<inspection_tool class="PyUnusedLocalInspection" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
</profile>"""))
assertFalse(profile.getToolsOrNull("PyUnusedParameterInspection", null)!!.isEnabled)
assertFalse(profile.getToolsOrNull("PyUnusedFunctionInspection", null)!!.isEnabled)
}
// The ignoreLambdaParameters option used to live on PyUnusedLocalInspection; it must carry over to the parameter inspection.
fun testIgnoreLambdaParametersOptionMigrates() {
profile.readExternal(JDOMUtil.load("""
<profile version="1.0">
<option name="myName" value="Test" />
<inspection_tool class="PyUnusedLocalInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoreLambdaParameters" value="false" />
</inspection_tool>
</profile>"""))
val tool = profile.getToolsOrNull("PyUnusedParameterInspection", null)!!.tool.tool as PyUnusedParameterInspection
assertFalse(tool.ignoreLambdaParameters)
}
}
@@ -33,7 +33,7 @@ import com.jetbrains.python.inspections.PyTypeCheckerInspection;
import com.jetbrains.python.inspections.PyUnboundLocalVariableInspection;
import com.jetbrains.python.inspections.PyUnusedImportsInspection;
import com.jetbrains.python.inspections.unresolvedReference.PyUnresolvedReferencesInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedLocalInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedParameterInspection;
import com.jetbrains.python.psi.PythonVisitorFilter;
import org.jetbrains.annotations.NotNull;
@@ -110,7 +110,7 @@ public class PyiInspectionsTest extends PyTestCase {
}
public void testPyiUnusedParameters() {
doPyiTest(PyUnusedLocalInspection.class);
doPyiTest(PyUnusedParameterInspection.class);
}
public void testPyiStatementEffect() {
@@ -21,7 +21,7 @@ import com.jetbrains.python.PyQuickFixTestCase;
import com.jetbrains.python.allure.Layers;
import com.jetbrains.python.allure.Subsystems;
import com.jetbrains.python.inspections.unresolvedReference.PyUnresolvedReferencesInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedLocalInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedParameterInspection;
import com.jetbrains.python.psi.LanguageLevel;
@TestDataPath("$CONTENT_ROOT/../testData//quickFixes/AddFieldQuickFixTest/")
@@ -52,16 +52,16 @@ public class PyAddFieldQuickFixTest extends PyQuickFixTestCase {
}
public void testFromUnusedParameter() { // PY-1398
doQuickFixTest(PyUnusedLocalInspection.class, "Add field 'foo' to class A");
doQuickFixTest(PyUnusedParameterInspection.class, "Add field 'foo' to class A");
}
// PY-14733
public void testAddFieldInitializationInsideEmptyInit() {
doQuickFixTest(PyUnusedLocalInspection.class, "Add field 'foo' to class A");
doQuickFixTest(PyUnusedParameterInspection.class, "Add field 'foo' to class A");
}
public void testFromUnusedParameterKeyword() { // PY-1602
doQuickFixTest(PyUnusedLocalInspection.class, "Add field 'foo' to class A");
doQuickFixTest(PyUnusedParameterInspection.class, "Add field 'foo' to class A");
}
// PY-21284
@@ -8,7 +8,7 @@ import com.jetbrains.python.PyQuickFixTestCase;
import com.jetbrains.python.allure.Layers;
import com.jetbrains.python.allure.Subsystems;
import com.jetbrains.python.documentation.docstrings.DocStringFormat;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedLocalInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedParameterInspection;
import com.jetbrains.python.psi.LanguageLevel;
@TestDataPath("$CONTENT_ROOT/../testData//quickFixes/PyRemoveParameterQuickFixTest/")
@@ -17,26 +17,26 @@ import com.jetbrains.python.psi.LanguageLevel;
public class PyRemoveParameterQuickFixTest extends PyQuickFixTestCase {
public void testParam() {
doQuickFixTest(PyUnusedLocalInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"));
doQuickFixTest(PyUnusedParameterInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"));
}
public void testKwParam() {
doQuickFixTest(PyUnusedLocalInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"));
doQuickFixTest(PyUnusedParameterInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"));
}
public void testDocstring() {
runWithDocStringFormat(DocStringFormat.REST, () ->
doQuickFixTest(PyUnusedLocalInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"))
doQuickFixTest(PyUnusedParameterInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"))
);
}
public void testUsage() {
doQuickFixTest(PyUnusedLocalInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"));
doQuickFixTest(PyUnusedParameterInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"));
}
public void testSingleStarTwoParam() {
runWithDocStringFormat(DocStringFormat.REST, () ->
doQuickFixTest(PyUnusedLocalInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"), LanguageLevel.PYTHON34)
doQuickFixTest(PyUnusedParameterInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"), LanguageLevel.PYTHON34)
);
}
@@ -45,7 +45,7 @@ public class PyRemoveParameterQuickFixTest extends PyQuickFixTestCase {
LanguageLevel.PYTHON34,
() -> {
final String testFileName = getTestName(true);
myFixture.enableInspections(PyUnusedLocalInspection.class);
myFixture.enableInspections(PyUnusedParameterInspection.class);
myFixture.configureByFile(testFileName + ".py");
myFixture.checkHighlighting(true, false, false);
final IntentionAction intentionAction = myFixture.getAvailableIntention(PyPsiBundle.message("QFIX.NAME.remove.parameter"));
@@ -56,11 +56,11 @@ public class PyRemoveParameterQuickFixTest extends PyQuickFixTestCase {
// PY-22971
public void testTopLevelOverloadsAndImplementation() {
doQuickFixTest(PyUnusedLocalInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"), LanguageLevel.PYTHON35);
doQuickFixTest(PyUnusedParameterInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"), LanguageLevel.PYTHON35);
}
// PY-22971
public void testOverloadsAndImplementationInClass() {
doQuickFixTest(PyUnusedLocalInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"), LanguageLevel.PYTHON35);
doQuickFixTest(PyUnusedParameterInspection.class, PyPsiBundle.message("QFIX.NAME.remove.parameter"), LanguageLevel.PYTHON35);
}
}
@@ -21,6 +21,7 @@ import com.jetbrains.python.PyQuickFixTestCase;
import com.jetbrains.python.allure.Layers;
import com.jetbrains.python.allure.Subsystems;
import com.jetbrains.python.inspections.PyReturnFromInitInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedFunctionInspection;
import com.jetbrains.python.inspections.unusedLocal.PyUnusedLocalInspection;
@TestDataPath("$CONTENT_ROOT/../testData//quickFixes/PyRemoveStatementQuickFixTest/")
@@ -37,7 +38,7 @@ public class PyRemoveStatementQuickFixTest extends PyQuickFixTestCase {
}
public void testFunction() {
doQuickFixTest(PyUnusedLocalInspection.class, PyPsiBundle.message("QFIX.NAME.remove.statement"));
doQuickFixTest(PyUnusedFunctionInspection.class, PyPsiBundle.message("QFIX.NAME.remove.statement"));
}
public void testVariable() {
@@ -5,6 +5,7 @@ import com.intellij.idea.TestFor
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
import com.jetbrains.python.fixtures.PyTestCase
import com.jetbrains.python.inspections.unusedLocal.PyUnusedLocalInspection
import com.jetbrains.python.inspections.unusedLocal.PyUnusedParameterInspection
import com.jetbrains.python.testing.pyTestParametrized.PyTestParametrizedInspection
/**
@@ -15,7 +16,8 @@ class PyTestFixtureAndParametrizedTest : PyTestCase() {
const val testSubfolder = "/testCompletion"
fun testInspectionStatic(fixture: CodeInsightTestFixture) {
fixture.configureByFile("test_for_inspection_test.py")
fixture.enableInspections(PyUnusedLocalInspection::class.java, PyTestParametrizedInspection::class.java)
fixture.enableInspections(PyUnusedLocalInspection::class.java, PyUnusedParameterInspection::class.java,
PyTestParametrizedInspection::class.java)
fixture.checkHighlighting(true, false, true)
}
}