mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-10-01 17:03:12 +07:00
PY-90285 inspections: name enclosing declaration in suppress actions
(cherry picked from commit 711a46ac788e7a30cc6e7316d85d8f3c89fbcdd9) GitOrigin-RevId: dc4f25279992a308f48a71335a79bed49e1b4d93
This commit is contained in:
committed by
intellij-monorepo-bot
parent
35e325f12e
commit
9c2ee70e4f
@@ -1394,10 +1394,9 @@ INSP.variance.checker.superfluous=Superfluous variance since the given constrain
|
||||
# PyInspectionsSuppressor
|
||||
INSP.python.suppressor.suppress.for.function=Suppress for a function
|
||||
INSP.python.suppressor.suppress.for.class=Suppress for a class
|
||||
INSP.python.suppressor.suppress.for.statement=Suppress for a statement
|
||||
INSP.python.suppressor.suppress.code.for.function=Suppress ''{0}'' for a function
|
||||
INSP.python.suppressor.suppress.code.for.class=Suppress ''{0}'' for a class
|
||||
INSP.python.suppressor.suppress.code.for.statement=Suppress ''{0}'' for a statement
|
||||
INSP.python.suppressor.suppress.for.statement=Suppress for this statement
|
||||
INSP.python.suppressor.suppress.for.function.named=Suppress for function ''{0}''
|
||||
INSP.python.suppressor.suppress.for.class.named=Suppress for class ''{0}''
|
||||
|
||||
find.usages.unnamed=<unnamed>
|
||||
find.usages.keyword.argument=keyword argument
|
||||
|
||||
+55
-20
@@ -11,40 +11,75 @@ import com.jetbrains.python.psi.PyClass;
|
||||
import com.jetbrains.python.psi.PyFunction;
|
||||
import com.jetbrains.python.psi.PyStatement;
|
||||
import com.jetbrains.python.psi.PyStatementList;
|
||||
import org.jetbrains.annotations.Nls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class PyInspectionsSuppressor implements InspectionSuppressor {
|
||||
private static final String PY_INCORRECT_DOCSTRING_INSPECTION_ID = new PyIncorrectDocstringInspection().getID();
|
||||
private static final String PY_MISSING_OR_EMPTY_DOCSTRING_INSPECTION_ID = new PyMissingOrEmptyDocstringInspection().getID();
|
||||
|
||||
@Override
|
||||
public SuppressQuickFix @NotNull [] getSuppressActions(@Nullable PsiElement element, @NotNull String toolId) {
|
||||
// PyTypeChecker owns its suppression UI entirely (PyTypeCheckerSuppressableProblemGroup); offer no blanket
|
||||
// actions for it, so the only suppressions shown are its granular per-code ones.
|
||||
if (PySuppressionUtil.INSTANCE.isCustomManaged(toolId)) {
|
||||
return SuppressQuickFix.EMPTY_ARRAY;
|
||||
}
|
||||
// Insert the industry-standard kebab-case alias (e.g. `method-overriding`) when the inspection has one,
|
||||
// so that is the form users adopt; the legacy `PyMethodOverriding` id keeps working for recognition.
|
||||
final String suppressId = suppressId(toolId);
|
||||
if (PY_INCORRECT_DOCSTRING_INSPECTION_ID.equals(toolId) || PY_MISSING_OR_EMPTY_DOCSTRING_INSPECTION_ID.equals(toolId)) {
|
||||
return new SuppressQuickFix[]{
|
||||
new PySuppressInspectionFix(suppressId, PyPsiBundle.message("INSP.python.suppressor.suppress.for.function"), PyFunction.class),
|
||||
new PySuppressInspectionFix(suppressId, PyPsiBundle.message("INSP.python.suppressor.suppress.for.class"), PyClass.class)
|
||||
};
|
||||
}
|
||||
else {
|
||||
return new SuppressQuickFix[]{
|
||||
new PySuppressInspectionFix(suppressId, PyPsiBundle.message("INSP.python.suppressor.suppress.for.statement"), PyStatement.class) {
|
||||
@Override
|
||||
public PsiElement getContainer(PsiElement context) {
|
||||
if (PsiTreeUtil.getParentOfType(context, PyStatementList.class, false, ScopeOwner.class) != null ||
|
||||
PsiTreeUtil.getParentOfType(context, PyFunction.class, PyClass.class) == null) {
|
||||
return super.getContainer(context);
|
||||
}
|
||||
return null;
|
||||
// Docstring inspections only attach to declarations, so they offer no per-statement suppression.
|
||||
final boolean includeStatement = !PY_INCORRECT_DOCSTRING_INSPECTION_ID.equals(toolId) &&
|
||||
!PY_MISSING_OR_EMPTY_DOCSTRING_INSPECTION_ID.equals(toolId);
|
||||
return createSuppressActions(suppressId, element, includeStatement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the "Suppress for this statement / for function 'f' / for class 'C'" quick fixes that insert a
|
||||
* {@code # noinspection <suppressId>} comment. Shared with {@link PyTypeCheckerSuppressableProblemGroup},
|
||||
* which passes one of its granular codes as {@code suppressId} but reuses the same declaration-naming wording.
|
||||
*/
|
||||
public static SuppressQuickFix @NotNull [] createSuppressActions(@NotNull String suppressId,
|
||||
@Nullable PsiElement element,
|
||||
boolean includeStatement) {
|
||||
List<SuppressQuickFix> fixes = new ArrayList<>(3);
|
||||
if (includeStatement) {
|
||||
fixes.add(new PySuppressInspectionFix(suppressId, PyPsiBundle.message("INSP.python.suppressor.suppress.for.statement"), PyStatement.class) {
|
||||
@Override
|
||||
public PsiElement getContainer(PsiElement context) {
|
||||
if (PsiTreeUtil.getParentOfType(context, PyStatementList.class, false, ScopeOwner.class) != null ||
|
||||
PsiTreeUtil.getParentOfType(context, PyFunction.class, PyClass.class) == null) {
|
||||
return super.getContainer(context);
|
||||
}
|
||||
},
|
||||
new PySuppressInspectionFix(suppressId, PyPsiBundle.message("INSP.python.suppressor.suppress.for.function"), PyFunction.class),
|
||||
new PySuppressInspectionFix(suppressId, PyPsiBundle.message("INSP.python.suppressor.suppress.for.class"), PyClass.class)
|
||||
};
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
fixes.add(new PySuppressInspectionFix(suppressId, functionText(element), PyFunction.class));
|
||||
fixes.add(new PySuppressInspectionFix(suppressId, classText(element), PyClass.class));
|
||||
return fixes.toArray(SuppressQuickFix.EMPTY_ARRAY);
|
||||
}
|
||||
|
||||
// Name the enclosing function/class in the action text (like Kotlin), falling back to the generic wording
|
||||
// when there is no enclosing declaration (e.g. when the list is requested from the inspection tool window).
|
||||
private static @Nls @NotNull String functionText(@Nullable PsiElement element) {
|
||||
PyFunction function = element == null ? null : PsiTreeUtil.getParentOfType(element, PyFunction.class);
|
||||
String name = function == null ? null : function.getName();
|
||||
return name != null
|
||||
? PyPsiBundle.message("INSP.python.suppressor.suppress.for.function.named", name)
|
||||
: PyPsiBundle.message("INSP.python.suppressor.suppress.for.function");
|
||||
}
|
||||
|
||||
private static @Nls @NotNull String classText(@Nullable PsiElement element) {
|
||||
PyClass pyClass = element == null ? null : PsiTreeUtil.getParentOfType(element, PyClass.class);
|
||||
String name = pyClass == null ? null : pyClass.getName();
|
||||
return name != null
|
||||
? PyPsiBundle.message("INSP.python.suppressor.suppress.for.class.named", name)
|
||||
: PyPsiBundle.message("INSP.python.suppressor.suppress.for.class");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,6 +22,13 @@ import java.util.regex.Pattern
|
||||
internal object PySuppressionUtil {
|
||||
private val SUPPRESS_PATTERN: Pattern = Pattern.compile(SuppressionUtil.COMMON_SUPPRESS_REGEXP)
|
||||
|
||||
/**
|
||||
* `PyTypeChecker` is excluded from the blanket alias mechanism: it fully manages its own granular suppression
|
||||
* codes ([PyTypeCheckerSuppressionCode]), so it gets no auto-derived `type-checker` alias and keeps the legacy
|
||||
* `PyTypeChecker` id as its catch-all.
|
||||
*/
|
||||
private const val PY_TYPE_CHECKER_INSPECTION_ID = "PyTypeChecker"
|
||||
|
||||
/**
|
||||
* Returns `true` if [element] is covered by a `# noinspection <suppressId>` comment placed above its
|
||||
* enclosing statement, function, or class.
|
||||
@@ -31,15 +38,23 @@ internal object PySuppressionUtil {
|
||||
isSuppressedForParent(element, PyFunction::class.java, suppressId) ||
|
||||
isSuppressedForParent(element, PyClass::class.java, suppressId)
|
||||
|
||||
/**
|
||||
* `true` for inspections that fully manage their own suppression UI (currently only `PyTypeChecker`, via
|
||||
* [PyTypeCheckerSuppressableProblemGroup]). [PyInspectionsSuppressor] offers no blanket suppress actions for
|
||||
* them, leaving the per-problem `SuppressableProblemGroup` as the sole source.
|
||||
*/
|
||||
fun isCustomManaged(toolId: String): Boolean = toolId == PY_TYPE_CHECKER_INSPECTION_ID
|
||||
|
||||
/**
|
||||
* Maps a legacy inspection id to its industry-standard kebab-case suppression alias by dropping the `Py`
|
||||
* product prefix and converting CamelCase to `kebab-case` (e.g. `PyMethodOverriding` -> `method-overriding`,
|
||||
* `PyUnresolvedReferences` -> `unresolved-references`).
|
||||
*
|
||||
* Returns `null` for ids that are not `Py`-prefixed inspection ids (e.g. platform inspections that also run
|
||||
* on Python files), which keep their original id only.
|
||||
* on Python files), which keep their original id only, and for [PY_TYPE_CHECKER_INSPECTION_ID].
|
||||
*/
|
||||
fun toSuppressionCode(toolId: String): String? {
|
||||
if (toolId == PY_TYPE_CHECKER_INSPECTION_ID) return null
|
||||
if (toolId.length < 3 || !toolId.startsWith("Py") || !toolId[2].isUpperCase()) return null
|
||||
return toolId.substring(2)
|
||||
.replace(Regex("([A-Z]+)([A-Z][a-z])"), "$1-$2")
|
||||
|
||||
+5
-26
@@ -3,22 +3,14 @@ package com.jetbrains.python.inspections
|
||||
|
||||
import com.intellij.codeInspection.SuppressIntentionAction
|
||||
import com.intellij.codeInspection.SuppressIntentionActionFromFix
|
||||
import com.intellij.codeInspection.SuppressQuickFix
|
||||
import com.intellij.codeInspection.SuppressableProblemGroup
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.jetbrains.python.PyPsiBundle
|
||||
import com.jetbrains.python.codeInsight.controlflow.ScopeOwner
|
||||
import com.jetbrains.python.inspections.quickfix.PySuppressInspectionFix
|
||||
import com.jetbrains.python.psi.PyClass
|
||||
import com.jetbrains.python.psi.PyFunction
|
||||
import com.jetbrains.python.psi.PyStatement
|
||||
import com.jetbrains.python.psi.PyStatementList
|
||||
|
||||
/**
|
||||
* Provides the Alt-Enter "Suppress '<code>' for a …" actions for a single [PyTypeCheckerInspection] problem
|
||||
* tagged with [code]. Each action inserts a `# noinspection <code>` comment, which
|
||||
* [PyTypeCheckerSuppressionUtil] then recognizes.
|
||||
* Provides the Alt-Enter "Suppress for this statement / for function 'f' / for class 'C'" actions for a single
|
||||
* [PyTypeCheckerInspection] problem tagged with [code]. The actions reuse
|
||||
* [PyInspectionsSuppressor.createSuppressActions] for identical, declaration-naming wording, but insert a
|
||||
* `# noinspection <code.id>` comment (e.g. `bad-return`), which [PySuppressionUtil] then recognizes.
|
||||
*
|
||||
* [getProblemName] returns `null` on purpose: a non-null name would re-key the highlight's
|
||||
* `HighlightDisplayKey` to the code (for which no inspection tool is registered), and the highlight would be
|
||||
@@ -28,20 +20,7 @@ internal class PyTypeCheckerSuppressableProblemGroup(private val code: PyTypeChe
|
||||
override fun getProblemName(): String? = null
|
||||
|
||||
override fun getSuppressActions(element: PsiElement?): Array<SuppressIntentionAction> {
|
||||
val id = code.id
|
||||
val fixes = arrayOf<SuppressQuickFix>(
|
||||
object : PySuppressInspectionFix(id, PyPsiBundle.message("INSP.python.suppressor.suppress.code.for.statement", id), PyStatement::class.java) {
|
||||
override fun getContainer(context: PsiElement?): PsiElement? {
|
||||
if (PsiTreeUtil.getParentOfType(context, PyStatementList::class.java, false, ScopeOwner::class.java) != null ||
|
||||
PsiTreeUtil.getParentOfType(context, PyFunction::class.java, PyClass::class.java) == null) {
|
||||
return super.getContainer(context)
|
||||
}
|
||||
return null
|
||||
}
|
||||
},
|
||||
PySuppressInspectionFix(id, PyPsiBundle.message("INSP.python.suppressor.suppress.code.for.function", id), PyFunction::class.java),
|
||||
PySuppressInspectionFix(id, PyPsiBundle.message("INSP.python.suppressor.suppress.code.for.class", id), PyClass::class.java),
|
||||
)
|
||||
val fixes = PyInspectionsSuppressor.createSuppressActions(code.id, element, true)
|
||||
return SuppressIntentionActionFromFix.convertBatchToSuppressIntentionActions(fixes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public class PySuppressInspectionsTest extends PyTestCase {
|
||||
public void testSuppressForStatement() {
|
||||
myFixture.configureByFile("inspections/suppress/suppressForStatement.py");
|
||||
myFixture.enableInspections(PyUnresolvedReferencesInspection.class);
|
||||
final List<IntentionAction> intentions = myFixture.filterAvailableIntentions("Suppress for a statement");
|
||||
final List<IntentionAction> intentions = myFixture.filterAvailableIntentions("Suppress for this statement");
|
||||
// Rename reference, Ignore unresolved references, Ignore all unresolved attributes of
|
||||
assertEquals(3, intentions.size());
|
||||
final IntentionAction suppressAction = intentions.get(0);
|
||||
@@ -63,4 +63,20 @@ public class PySuppressInspectionsTest extends PyTestCase {
|
||||
myFixture.enableInspections(PyUnresolvedReferencesInspection.class);
|
||||
myFixture.checkHighlighting(true, false, true);
|
||||
}
|
||||
|
||||
// The suppress actions name the enclosing function/class (like Kotlin), instead of a bare "a function"/"a class".
|
||||
@TestFor(issues="PY-90285")
|
||||
public void testSuppressActionNamesFunction() {
|
||||
myFixture.configureByText("a.py", "def f():\n print(x<caret>xx)");
|
||||
myFixture.enableInspections(PyUnresolvedReferencesInspection.class);
|
||||
assertFalse(myFixture.filterAvailableIntentions("Suppress for function 'f'").isEmpty());
|
||||
}
|
||||
|
||||
@TestFor(issues="PY-90285")
|
||||
public void testSuppressActionNamesClassAndMethod() {
|
||||
myFixture.configureByText("a.py", "class C:\n def m(self):\n print(x<caret>xx)");
|
||||
myFixture.enableInspections(PyUnresolvedReferencesInspection.class);
|
||||
assertFalse(myFixture.filterAvailableIntentions("Suppress for class 'C'").isEmpty());
|
||||
assertFalse(myFixture.filterAvailableIntentions("Suppress for function 'm'").isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user