PY-19917 Support type ignore comments

(cherry picked from commit 693b7f2bf037ac9fd464e330390c76ee5268c023)

IJ-MR-5695

GitOrigin-RevId: 354a4ba98e0ff01787ae0718c8643772b118fc9f
This commit is contained in:
andrey.matveev
2021-02-15 16:45:22 +00:00
committed by intellij-monorepo-bot
parent 4ca740f0e6
commit 3322cedc0d
9 changed files with 301 additions and 23 deletions
@@ -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;
}
}
@@ -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")
@@ -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;
}
}
@@ -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")
}
}
@@ -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
}
}
@@ -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)
}
}