mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-19917 Support type ignore comments
(cherry picked from commit 693b7f2bf037ac9fd464e330390c76ee5268c023) IJ-MR-5695 GitOrigin-RevId: 354a4ba98e0ff01787ae0718c8643772b118fc9f
This commit is contained in:
committed by
intellij-monorepo-bot
parent
4ca740f0e6
commit
3322cedc0d
@@ -662,4 +662,21 @@ public final class PyPsiUtils {
|
||||
return element.getText();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PsiComment findSameLineComment(@NotNull PsiElement elem) {
|
||||
// If `elem` is a compound multi-line element, stick to its first line nonetheless
|
||||
PsiElement next = PsiTreeUtil.getDeepestFirst(elem);
|
||||
do {
|
||||
if (next instanceof PsiComment) {
|
||||
return (PsiComment)next;
|
||||
}
|
||||
if (next != elem && next.textContains('\n')) {
|
||||
break;
|
||||
}
|
||||
next = PsiTreeUtil.nextLeaf(next);
|
||||
}
|
||||
while (next != null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -101,7 +101,8 @@ public class PyTypingTypeProvider extends PyTypeProviderBase {
|
||||
private static final String PY3_TEXT_FILE_TYPE = "typing.TextIO";
|
||||
|
||||
private static final Pattern TYPE_COMMENT_PATTERN = Pattern.compile("# *type: *([^#]+) *(#.*)?");
|
||||
public static final Pattern IGNORE_PATTERN = Pattern.compile("^ignore(\\[ *[^ ,\\]]+ *(, *[^ ,\\]]+ *)*\\])?$");
|
||||
public static final Pattern TYPE_IGNORE_PATTERN = Pattern.compile("# *type: *ignore(\\[ *[^ ,\\]]+ *(, *[^ ,\\]]+ *)*\\])? *($|(#.*))",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
public static final ImmutableMap<String, String> BUILTIN_COLLECTION_CLASSES = ImmutableMap.<String, String>builder()
|
||||
.put(LIST, "list")
|
||||
|
||||
+1
-1
@@ -133,7 +133,7 @@ class PyTypeHintsInspection : PyInspection() {
|
||||
|
||||
if (node is PyTypeCommentOwner &&
|
||||
node is PyAnnotationOwner &&
|
||||
node.typeCommentAnnotation.let { it != null && !PyTypingTypeProvider.IGNORE_PATTERN.matcher(it).matches() }) {
|
||||
node.typeComment?.text.let { it != null && !PyTypingTypeProvider.TYPE_IGNORE_PATTERN.matcher(it).matches() }) {
|
||||
val message = PyPsiBundle.message("INSP.type.hints.type.specified.both.in.type.comment.and.annotation")
|
||||
|
||||
if (node is PyFunction) {
|
||||
|
||||
@@ -380,6 +380,10 @@
|
||||
<lang.inspectionSuppressor language="Python" implementationClass="com.jetbrains.python.inspections.flake8.Flake8InspectionSuppressor"/>
|
||||
<daemon.changeLocalityDetector implementation="com.jetbrains.python.inspections.flake8.Flake8SuppressingCommentLocalityDetector"/>
|
||||
|
||||
<!-- # type: ignore -->
|
||||
<lang.inspectionSuppressor language="Python" implementationClass="com.jetbrains.python.inspections.typeignore.TypeIgnoreInspectionSuppressor"/>
|
||||
<daemon.changeLocalityDetector implementation="com.jetbrains.python.inspections.typeignore.TypeIgnoreSuppressingCommentLocalityDetector"/>
|
||||
|
||||
<!-- Pipenv -->
|
||||
<editorFactoryListener implementation="com.jetbrains.python.sdk.pipenv.PipEnvPipFileWatcher"/>
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ import static com.jetbrains.python.psi.PyUtil.as;
|
||||
public class PyTypingAnnotationInjector extends PyInjectorBase {
|
||||
public static final Pattern RE_TYPING_ANNOTATION = Pattern.compile("\\s*\\S+(\\[.*\\])?\\s*");
|
||||
|
||||
private static final Pattern TYPE_IGNORE_PATTERN = Pattern.compile("#\\s*type:\\s*ignore(\\[[^]#]*])?\\s*($|(#.*))", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@Override
|
||||
protected PyInjectionUtil.InjectionResult registerInjection(@NotNull MultiHostRegistrar registrar, @NotNull PsiElement context) {
|
||||
// Handles only string literals containing quoted types
|
||||
@@ -85,7 +87,7 @@ public class PyTypingAnnotationInjector extends PyInjectorBase {
|
||||
final String annotationText = PyTypingTypeProvider.getTypeCommentValue(text);
|
||||
if (annotationText != null) {
|
||||
final Language language;
|
||||
if (PyTypingTypeProvider.IGNORE_PATTERN.matcher(annotationText).matches()) {
|
||||
if (PyTypingTypeProvider.TYPE_IGNORE_PATTERN.matcher(text).matches()) {
|
||||
language = null;
|
||||
}
|
||||
else if (isFunctionTypeComment(host)) {
|
||||
@@ -137,4 +139,11 @@ public class PyTypingAnnotationInjector extends PyInjectorBase {
|
||||
private static boolean isTypingAnnotation(@NotNull String s) {
|
||||
return RE_TYPING_ANNOTATION.matcher(s).matches();
|
||||
}
|
||||
|
||||
public static boolean isTypeIgnoreComment(@NotNull PsiElement comment) {
|
||||
if (!(comment instanceof PsiComment)) return false;
|
||||
String text = comment.getText();
|
||||
if (text == null) return false;
|
||||
return TYPE_IGNORE_PATTERN.matcher(text).matches();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import com.intellij.codeInspection.SuppressQuickFix;
|
||||
import com.intellij.psi.PsiComment;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiFileSystemItem;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.jetbrains.python.psi.PyFile;
|
||||
import com.jetbrains.python.psi.impl.PyPsiUtils;
|
||||
import one.util.streamex.StreamEx;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -62,7 +63,7 @@ public class Flake8InspectionSuppressor implements InspectionSuppressor {
|
||||
return false;
|
||||
}
|
||||
|
||||
final PsiComment comment = findSameLineComment(element);
|
||||
final PsiComment comment = PyPsiUtils.findSameLineComment(element);
|
||||
if (comment != null) {
|
||||
final Set<String> givenCodes = extractNoqaCodes(comment);
|
||||
if (givenCodes != null) {
|
||||
@@ -82,7 +83,11 @@ public class Flake8InspectionSuppressor implements InspectionSuppressor {
|
||||
*/
|
||||
@Nullable
|
||||
public static Set<String> extractNoqaCodes(@NotNull PsiComment comment) {
|
||||
final Matcher matcher = NOQA_COMMENT_PATTERN.matcher(comment.getText());
|
||||
String commentText = comment.getText();
|
||||
if (commentText == null) return null;
|
||||
int noqaOffset = StringUtils.lowerCase(commentText).indexOf("# noqa");
|
||||
String noqaSuffix = StringUtils.substring(commentText, noqaOffset);
|
||||
final Matcher matcher = NOQA_COMMENT_PATTERN.matcher(noqaSuffix);
|
||||
if (matcher.matches()) {
|
||||
final String codeList = matcher.group("codes");
|
||||
if (codeList != null) {
|
||||
@@ -98,21 +103,4 @@ public class Flake8InspectionSuppressor implements InspectionSuppressor {
|
||||
public SuppressQuickFix[] getSuppressActions(@Nullable PsiElement element, @NotNull String toolId) {
|
||||
return SuppressQuickFix.EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static PsiComment findSameLineComment(@NotNull PsiElement elem) {
|
||||
// If `elem` is a compound multi-line element, stick to its first line nonetheless
|
||||
PsiElement next = PsiTreeUtil.getDeepestFirst(elem);
|
||||
do {
|
||||
if (next instanceof PsiComment) {
|
||||
return (PsiComment)next;
|
||||
}
|
||||
if (next != elem && next.textContains('\n')) {
|
||||
break;
|
||||
}
|
||||
next = PsiTreeUtil.nextLeaf(next);
|
||||
}
|
||||
while (next != null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.jetbrains.python.inspections.typeignore
|
||||
|
||||
import com.intellij.codeInspection.InspectionSuppressor
|
||||
import com.intellij.codeInspection.SuppressQuickFix
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.jetbrains.python.codeInsight.typing.PyTypingAnnotationInjector
|
||||
import com.jetbrains.python.psi.PyFile
|
||||
import com.jetbrains.python.psi.impl.PyPsiUtils
|
||||
|
||||
class TypeIgnoreInspectionSuppressor: InspectionSuppressor {
|
||||
|
||||
override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean {
|
||||
if (element is PsiFile) return false
|
||||
if (element.containingFile !is PyFile) return false
|
||||
if (toolId !in inspectionsToSuppress) return false
|
||||
val comment = PyPsiUtils.findSameLineComment(element) ?: return false
|
||||
return PyTypingAnnotationInjector.isTypeIgnoreComment(comment)
|
||||
}
|
||||
|
||||
override fun getSuppressActions(element: PsiElement?, toolId: String): Array<SuppressQuickFix> {
|
||||
return SuppressQuickFix.EMPTY_ARRAY
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val inspectionsToSuppress = listOf("PyUnresolvedReferences",
|
||||
"PyTypeHints",
|
||||
"PyTypeChecker",
|
||||
"PyRedeclaration",
|
||||
"PyArgumentList",
|
||||
"PyFinal",
|
||||
"PyProtocol",
|
||||
"PyTypedDict")
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.jetbrains.python.inspections.typeignore
|
||||
|
||||
import com.intellij.codeInsight.daemon.ChangeLocalityDetector
|
||||
import com.intellij.psi.PsiComment
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.jetbrains.python.codeInsight.typing.PyTypingAnnotationInjector
|
||||
import com.jetbrains.python.psi.PyStatement
|
||||
|
||||
class TypeIgnoreSuppressingCommentLocalityDetector: ChangeLocalityDetector {
|
||||
override fun getChangeHighlightingDirtyScopeFor(changedElement: PsiElement): PsiElement? {
|
||||
if (changedElement is PsiComment && PyTypingAnnotationInjector.isTypeIgnoreComment(changedElement)) {
|
||||
return PsiTreeUtil.getParentOfType(changedElement, PyStatement::class.java)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
package com.jetbrains.python.inspections.typeignore
|
||||
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.spellchecker.inspections.SpellCheckingInspection
|
||||
import com.jetbrains.python.PythonFileType
|
||||
import com.jetbrains.python.fixtures.PyTestCase
|
||||
import com.jetbrains.python.inspections.*
|
||||
import com.jetbrains.python.inspections.unresolvedReference.PyUnresolvedReferencesInspection
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
|
||||
class TypeIgnoreInspectionSuppressorTest: PyTestCase() {
|
||||
|
||||
fun testCaseInsensitiveness() {
|
||||
doTestByText("""
|
||||
def foo(x: str):
|
||||
print(x.bar) # TyPe: IGnore
|
||||
""")
|
||||
}
|
||||
|
||||
fun testWhitespacesInsensitiveness() {
|
||||
doTestByText("""
|
||||
def foo(x: str):
|
||||
print(x.bar) #type: ignore
|
||||
""")
|
||||
}
|
||||
|
||||
fun testPlainTextCommentAfterwards() {
|
||||
doTestByText("""
|
||||
def foo(x: str):
|
||||
print(x.bar) # type: ignore # plain text comment
|
||||
""")
|
||||
}
|
||||
|
||||
fun testPlainTextAfterwards() {
|
||||
doTestByText("""
|
||||
def foo(x: str):
|
||||
print(x.<warning descr="Unresolved attribute reference 'bar' for class 'str'">bar</warning>) # type: <warning descr="Unresolved reference 'ignore'">ignore</warning><error descr="End of statement expected"> </error><warning descr="Unresolved reference 'plaintextnotcomment'">plaintextnotcomment</warning>
|
||||
""")
|
||||
}
|
||||
|
||||
fun testSpaceAfterTypePrefix() {
|
||||
doTestByText("""
|
||||
def foo(x: str):
|
||||
print(x.<warning descr="Unresolved attribute reference 'bar' for class 'str'">bar</warning>) # type : ignore
|
||||
""")
|
||||
}
|
||||
|
||||
fun testTypeIgnoreD() {
|
||||
doTestByText("""
|
||||
def foo(x: str):
|
||||
print(x.<warning descr="Unresolved attribute reference 'bar' for class 'str'">bar</warning>) # type: <warning descr="Unresolved reference 'ignoreD'">ignoreD</warning>
|
||||
""")
|
||||
}
|
||||
|
||||
fun testWithNoqa() {
|
||||
myFixture.enableInspections(SpellCheckingInspection::class.java)
|
||||
doTestByText("""
|
||||
def foo(x: str):
|
||||
print(x.bar + 'ajsd') # type: ignore # noqa
|
||||
""")
|
||||
}
|
||||
|
||||
fun testTwoCodes() {
|
||||
doTestByText("""
|
||||
def foo(x: str):
|
||||
print(x.bar) #type: ignore[foo , bar-baz]
|
||||
""")
|
||||
}
|
||||
|
||||
fun testTypeIgnoreCodesIgnoredCurrently() {
|
||||
doTestByText("""
|
||||
def foo(x: str):
|
||||
print(x.<warning descr="Unresolved attribute reference 'bar' for class 'str'">bar</warning>)
|
||||
print(x.bar) # type: ignore
|
||||
print(x.bar) # type: ignore[attr-defined]
|
||||
print(x.bar) # type: ignore[call-arg]
|
||||
print(x.bar) # type: ignore[whatever]
|
||||
""")
|
||||
}
|
||||
|
||||
fun testIgnoreType() {
|
||||
doTestByText("""
|
||||
print(2 + 'foo') # type: ignore
|
||||
print(2 + <warning descr="Expected type 'int', got 'str' instead">'foo'</warning>)
|
||||
""")
|
||||
}
|
||||
|
||||
fun testIgnoreUnresolvedReferenceAttribute() {
|
||||
doTestByText("""
|
||||
def foo(x: str):
|
||||
print(x.bar) # type: ignore
|
||||
print(x.<warning descr="Unresolved attribute reference 'bar' for class 'str'">bar</warning>)
|
||||
""")
|
||||
}
|
||||
|
||||
fun testIgnoreUnresolvedReferenceImport() {
|
||||
doTestByText("""
|
||||
import frobnicate # type: ignore
|
||||
<warning descr="Unused import statement 'import frobnicate1'">import <error descr="No module named 'frobnicate1'">frobnicate1</error></warning>
|
||||
""")
|
||||
}
|
||||
|
||||
fun testTypeHint() {
|
||||
doTestByText("""
|
||||
class A:
|
||||
pass
|
||||
|
||||
A.foo = 10 # type: ignore[attr-defined]
|
||||
""")
|
||||
}
|
||||
|
||||
fun testIgnoreUnexpectedArgument() {
|
||||
doTestByText("""
|
||||
def foo(s: str) -> None:
|
||||
print(s)
|
||||
|
||||
foo('foo', 'bar') # type: ignore
|
||||
foo('foo', <warning descr="Unexpected argument">'bar'</warning>)
|
||||
""")
|
||||
}
|
||||
|
||||
fun testIgnoreUnexpectedReturnType() {
|
||||
doTestByText("""
|
||||
def func(x: int) -> str:
|
||||
return x + 1 # type: ignore
|
||||
|
||||
def func1(x: int) -> str:
|
||||
return <warning descr="Expected type 'str', got 'int' instead">x + 1</warning>
|
||||
""")
|
||||
}
|
||||
|
||||
fun testIgnoreRedeclaredWithoutUsage() {
|
||||
doTestByText("""
|
||||
class A:
|
||||
def __init__(self, x: int) -> None: ...
|
||||
|
||||
class A: # type: ignore
|
||||
def __init__(self, x: str) -> None: ...
|
||||
|
||||
class <warning descr="Redeclared 'A' defined above without usage">A</warning>:
|
||||
def __init__(self, x: str) -> None: ...
|
||||
""")
|
||||
}
|
||||
|
||||
fun testIgnoreFinal() {
|
||||
doTestByText("""
|
||||
from typing_extensions import final
|
||||
@final
|
||||
class A:
|
||||
pass
|
||||
|
||||
class B(A): # type: ignore
|
||||
pass
|
||||
|
||||
class <warning descr="'A' is marked as '@final' and should not be subclassed">C</warning>(A):
|
||||
pass
|
||||
""")
|
||||
}
|
||||
|
||||
fun testIgnoreProtocol() {
|
||||
doTestByText("""
|
||||
from typing import NewType, Protocol
|
||||
|
||||
class Id1(Protocol):
|
||||
code: int
|
||||
|
||||
UserId1 = NewType('UserId1', Id1) # type: ignore
|
||||
UserId2 = NewType('UserId2', <warning descr="NewType cannot be used with protocol classes">Id1</warning>)
|
||||
""")
|
||||
}
|
||||
|
||||
fun testIgnoreTypedDict() {
|
||||
doTestByText("""
|
||||
from typing import TypedDict
|
||||
|
||||
class Movie(TypedDict, metaclass=Meta): # type: ignore
|
||||
name: str
|
||||
|
||||
class Movie1(TypedDict, <warning descr="Specifying a metaclass is not allowed in TypedDict">metaclass=<error descr="Unresolved reference 'Meta'">Meta</error></warning>):
|
||||
name: str
|
||||
""")
|
||||
}
|
||||
|
||||
private fun doTestByText(notTrimmedText: String) {
|
||||
val text = notTrimmedText.trimIndent()
|
||||
runWithLanguageLevel(LanguageLevel.getLatest()) {
|
||||
myFixture.enableInspections(inspections)
|
||||
val currentFile: PsiFile = myFixture.configureByText(PythonFileType.INSTANCE, text)
|
||||
myFixture.checkHighlighting()
|
||||
assertSdkRootsNotParsed(currentFile)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val inspections = listOf(PyUnresolvedReferencesInspection::class.java,
|
||||
PyTypeHintsInspection::class.java,
|
||||
PyTypeCheckerInspection::class.java,
|
||||
PyArgumentListInspection::class.java,
|
||||
PyRedeclarationInspection::class.java,
|
||||
PyFinalInspection::class.java,
|
||||
PyProtocolInspection::class.java,
|
||||
PyTypedDictInspection::class.java)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user