mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[python] PY-85601 Add "invalid escape sequence" inspection
GitOrigin-RevId: d750f06efc5e42678073de084ecb2976acedd4b9
This commit is contained in:
committed by
intellij-monorepo-bot
parent
3e223423f2
commit
788e8300e4
+22
@@ -0,0 +1,22 @@
|
||||
<html>
|
||||
<body>
|
||||
Reports invalid escape sequences in string and bytes literals.
|
||||
|
||||
<p>
|
||||
Starting from Python 3.6, escape sequences that are not recognized (such as <code>\.</code>) produce a <code>SyntaxWarning</code>.
|
||||
In future Python versions, they will become a <code>SyntaxError</code>.
|
||||
</p>
|
||||
<p>
|
||||
To fix this, you can either escape the backslash (e.g., <code>\\.</code>) or use a raw string (e.g., <code>r'\.'</code>).
|
||||
</p>
|
||||
<p><b>Example:</b></p>
|
||||
<pre><code>
|
||||
# Warning: Invalid escape sequence '\.'
|
||||
print('\.')
|
||||
|
||||
# Correct
|
||||
print(r'\.')
|
||||
print('\\.')
|
||||
</code></pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -269,6 +269,7 @@
|
||||
<localInspection language="Python" shortName="PyAssertTypeInspection" suppressId="PyAssertType" bundle="messages.PyPsiBundle" key="INSP.NAME.assert.type" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyAssertTypeInspection"/>
|
||||
<localInspection language="Python" shortName="PyEnumInspection" suppressId="PyEnum" bundle="messages.PyPsiBundle" key="INSP.NAME.enum" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyEnumInspection"/>
|
||||
<localInspection language="Python" shortName="PyNewTypeInspection" suppressId="PyNewType" bundle="messages.PyPsiBundle" key="INSP.NAME.new.type" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyNewTypeInspection"/>
|
||||
<localInspection language="Python" shortName="PyInvalidEscapeSequenceInspection" suppressId="PyInvalidEscapeSequence" bundle="messages.PyPsiBundle" key="INSP.NAME.invalid.escape.sequence" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyInvalidEscapeSequenceInspection"/>
|
||||
|
||||
<intentionAction>
|
||||
<language>Python</language>
|
||||
|
||||
@@ -1378,3 +1378,11 @@ QFIX.remove.cast.call=Remove 'cast' call
|
||||
QFIX.add.source.root.notification.text=''{0}'' has been marked as source root
|
||||
QFIX.add.source.root.notification.ok=Ok
|
||||
QFIX.add.source.root.notification.revert=Revert
|
||||
|
||||
# PyInvalidEscapeSequenceInspection
|
||||
INSP.NAME.invalid.escape.sequence=Invalid escape sequence
|
||||
INSP.invalid.escape.sequence=Invalid escape sequence ''{0}''
|
||||
|
||||
# Quick fixes for PyInvalidEscapeSequenceInspection
|
||||
QFIX.escape.backslash=Escape backslash
|
||||
QFIX.convert.to.raw.string=Convert to raw string
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package com.jetbrains.python.inspections
|
||||
|
||||
import com.intellij.codeInspection.LocalInspectionToolSession
|
||||
import com.intellij.codeInspection.LocalQuickFix
|
||||
import com.intellij.codeInspection.ProblemHighlightType
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiElementVisitor
|
||||
import com.jetbrains.python.PyPsiBundle
|
||||
import com.jetbrains.python.inspections.quickfix.PyConvertToRawStringQuickFix
|
||||
import com.jetbrains.python.inspections.quickfix.PyEscapeBackslashQuickFix
|
||||
import com.jetbrains.python.psi.PyFormattedStringElement
|
||||
import com.jetbrains.python.psi.PyStringElement
|
||||
import com.jetbrains.python.psi.PyStringLiteralExpression
|
||||
import com.jetbrains.python.psi.impl.PyStringLiteralDecoder
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext
|
||||
|
||||
class PyInvalidEscapeSequenceInspection : PyInspection() {
|
||||
|
||||
override fun buildVisitor(
|
||||
holder: ProblemsHolder,
|
||||
isOnTheFly: Boolean,
|
||||
session: LocalInspectionToolSession,
|
||||
): PsiElementVisitor = Visitor(holder, PyInspectionVisitor.getContext(session))
|
||||
|
||||
private class Visitor(holder: ProblemsHolder?, context: TypeEvalContext) : PyInspectionVisitor(holder, context) {
|
||||
|
||||
override fun visitPyStringLiteralExpression(node: PyStringLiteralExpression) =
|
||||
node.stringElements.forEach { processString(it, node) }
|
||||
|
||||
private fun processString(element: PyStringElement, node: PyStringLiteralExpression) {
|
||||
if (element.prefix.contains("r", ignoreCase = true)) return
|
||||
|
||||
val content = element.contentRange.substring(element.text)
|
||||
val offsetInNode = element.textRange.startOffset + element.contentRange.startOffset - node.textRange.startOffset
|
||||
|
||||
val fstringRanges = if (element is PyFormattedStringElement) {
|
||||
element.literalPartRanges.asSequence().map { it.shiftLeft(element.contentRange.startOffset) }
|
||||
}
|
||||
else {
|
||||
sequenceOf(TextRange(0, content.length))
|
||||
}
|
||||
|
||||
val validEscapes = fstringRanges.flatMap { range ->
|
||||
PyStringLiteralDecoder.PATTERN_ESCAPE.toRegex()
|
||||
.findAll(content, range.startOffset)
|
||||
.takeWhile { it.range.last < range.endOffset }
|
||||
.map { it.range }
|
||||
}
|
||||
.iterator()
|
||||
val hasValidEscapes = validEscapes.hasNext()
|
||||
var curValidEscape = if (hasValidEscapes) validEscapes.next() else null
|
||||
|
||||
for (range in fstringRanges) {
|
||||
var charIndex = range.startOffset
|
||||
while (charIndex < range.endOffset - 1) {
|
||||
|
||||
if (content[charIndex] != '\\') {
|
||||
charIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
while (curValidEscape?.let { it.last < charIndex } == true) {
|
||||
curValidEscape = if (validEscapes.hasNext()) validEscapes.next() else null
|
||||
}
|
||||
if (curValidEscape?.contains(charIndex) == true) {
|
||||
charIndex = curValidEscape.last + 1
|
||||
continue
|
||||
}
|
||||
|
||||
val problemOffset = offsetInNode + charIndex
|
||||
|
||||
val fixes = LocalQuickFix.notNullElements(
|
||||
PyEscapeBackslashQuickFix(problemOffset),
|
||||
if (!hasValidEscapes) PyConvertToRawStringQuickFix(problemOffset) else null
|
||||
)
|
||||
|
||||
registerProblem(
|
||||
node,
|
||||
PyPsiBundle.message("INSP.invalid.escape.sequence", "\\${content[charIndex + 1]}"),
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||
null,
|
||||
TextRange(problemOffset, problemOffset + 2),
|
||||
*fixes
|
||||
)
|
||||
charIndex += 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.jetbrains.python.inspections.quickfix
|
||||
|
||||
import com.intellij.codeInsight.intention.LowPriorityAction
|
||||
import com.intellij.modcommand.ModPsiUpdater
|
||||
import com.intellij.modcommand.PsiUpdateModCommandQuickFix
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.jetbrains.python.PyPsiBundle
|
||||
import com.jetbrains.python.psi.PyElementGenerator
|
||||
import com.jetbrains.python.psi.PyStringLiteralExpression
|
||||
|
||||
class PyConvertToRawStringQuickFix(private val offsetInElement: Int) : PsiUpdateModCommandQuickFix(), LowPriorityAction {
|
||||
override fun getFamilyName(): String = PyPsiBundle.message("QFIX.convert.to.raw.string")
|
||||
|
||||
override fun applyFix(project: Project, element: PsiElement, updater: ModPsiUpdater) {
|
||||
if (element !is PyStringLiteralExpression) return
|
||||
|
||||
val part = element.stringElements.find { it.textRangeInParent.contains(offsetInElement) } ?: return
|
||||
if (part.prefix.contains("r", ignoreCase = true)) return
|
||||
|
||||
val newText = "r" + part.text
|
||||
|
||||
val newPart = PyElementGenerator.getInstance(project)
|
||||
.createStringLiteralAlreadyEscaped(newText)
|
||||
.stringElements.firstOrNull() ?: return
|
||||
part.replace(newPart)
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.jetbrains.python.inspections.quickfix
|
||||
|
||||
import com.intellij.modcommand.ModPsiUpdater
|
||||
import com.intellij.modcommand.PsiUpdateModCommandQuickFix
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.jetbrains.python.PyPsiBundle
|
||||
import com.jetbrains.python.psi.PyElementGenerator
|
||||
import com.jetbrains.python.psi.PyStringLiteralExpression
|
||||
|
||||
class PyEscapeBackslashQuickFix(private val offsetInElement: Int) : PsiUpdateModCommandQuickFix() {
|
||||
override fun getFamilyName(): String = PyPsiBundle.message("QFIX.escape.backslash")
|
||||
|
||||
override fun applyFix(project: Project, element: PsiElement, updater: ModPsiUpdater) {
|
||||
if (element !is PyStringLiteralExpression) return
|
||||
|
||||
if (offsetInElement >= element.textLength || element.text[offsetInElement] != '\\') return
|
||||
|
||||
val newText = StringBuilder(element.text).insert(offsetInElement, "\\").toString()
|
||||
|
||||
val newElement = PyElementGenerator.getInstance(project)
|
||||
.createStringLiteralAlreadyEscaped(newText)
|
||||
element.replace(newElement)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
s1 = "<warning descr="Invalid escape sequence '\.'">\.</warning>"
|
||||
|
||||
s2 = "<warning descr="Invalid escape sequence '\z'">\z</warning>"
|
||||
|
||||
s3 = "\a \b \f \n \r \t \v \\ \' \" \0 \x41 \u1234 \U00012345 \N{SNOWMAN}"
|
||||
|
||||
s4 = r"\."
|
||||
|
||||
s5 = " <warning descr="Invalid escape sequence '\.'">\.</warning> <warning descr="Invalid escape sequence '\z'">\z</warning> "
|
||||
@@ -0,0 +1,9 @@
|
||||
b1 = b"<warning descr="Invalid escape sequence '\.'">\.</warning>"
|
||||
|
||||
b2 = b"<warning descr="Invalid escape sequence '\z'">\z</warning>"
|
||||
|
||||
b3 = b"\a \b \f \n \r \t \v \\ \' \" \0 \x41 \u1234 \U00012345 \N{SNOWMAN}"
|
||||
|
||||
b4 = rb"\."
|
||||
|
||||
b5 = b" <warning descr="Invalid escape sequence '\.'">\.</warning> <warning descr="Invalid escape sequence '\z'">\z</warning> "
|
||||
+1
@@ -0,0 +1 @@
|
||||
s = "postfix" '\n<caret>\.' "suffix"
|
||||
+1
@@ -0,0 +1 @@
|
||||
s = "prefix" r'\.' "suffix"
|
||||
+1
@@ -0,0 +1 @@
|
||||
s = "prefix" '<caret>\.' "suffix"
|
||||
@@ -0,0 +1 @@
|
||||
s = "prefix" '\\.' "suffix"
|
||||
+1
@@ -0,0 +1 @@
|
||||
s = "prefix" '<caret>\.' "suffix"
|
||||
@@ -0,0 +1,44 @@
|
||||
f1 = f"<warning descr="Invalid escape sequence '\.'">\.</warning>"
|
||||
|
||||
f2 = f"\a \b \f \n \r \t \v \\ \' \" \0 \x41 \u1234 \U00012345 \N{SNOWMAN}"
|
||||
|
||||
f3 = f"foo {1} <warning descr="Invalid escape sequence '\ '">\ </warning>"
|
||||
|
||||
f4 = f"foo { f'<warning descr="Invalid escape sequence '\.'">\.</warning>' }"
|
||||
|
||||
f5 = f"level1 { f'level2 { f"level3 <warning descr="Invalid escape sequence '\.'">\.</warning>" }' } <warning descr="Invalid escape sequence '\.'">\.</warning>"
|
||||
|
||||
f6 = f"valid \n invalid <warning descr="Invalid escape sequence '\.'">\.</warning> valid \t"
|
||||
|
||||
f7 = f"{{ <warning descr="Invalid escape sequence '\.'">\.</warning> }}"
|
||||
|
||||
f8 = f"<warning descr="Invalid escape sequence '\{'">\{</warning>{"
|
||||
|
||||
f9 = f"""
|
||||
<warning descr="Invalid escape sequence '\.'">\.</warning>
|
||||
{1}
|
||||
valid \n
|
||||
<warning descr="Invalid escape sequence '\z'">\z</warning>
|
||||
"""
|
||||
|
||||
f10 = f"text <warning descr="Invalid escape sequence '\.'">\.</warning> { '\n' } text"
|
||||
|
||||
f11 = f"list {[1, 2]} set { {1, 2} } dict { {'k': 'v'} } <warning descr="Invalid escape sequence '\.'">\.</warning>"
|
||||
|
||||
# \n and \x are escape sequences, while \z is not:
|
||||
f12 = f"expression { 1 + \
|
||||
n } <warning descr="Invalid escape sequence '\.'">\.</warning>"
|
||||
|
||||
f13 = f"nested { f'inner { 1 + \
|
||||
x }' } "
|
||||
|
||||
f16 = f"expression { 1 + \
|
||||
z } <warning descr="Invalid escape sequence '\.'">\.</warning>"
|
||||
|
||||
f14 = f" { '\n' } { '<warning descr="Invalid escape sequence '\.'">\.</warning>' } "
|
||||
|
||||
f15 = f" { {'key': '\n', 'val': '<warning descr="Invalid escape sequence '\.'">\.</warning>'} } "
|
||||
|
||||
rf1 = rf"\."
|
||||
rf2 = rf"foo {1} \."
|
||||
rf3 = fr"foo {1} \."
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// 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.jetbrains.python.PyPsiBundle
|
||||
import com.jetbrains.python.fixtures.PyInspectionTestCase
|
||||
|
||||
class PyInvalidEscapeSequenceInspectionTest : PyInspectionTestCase() {
|
||||
|
||||
override fun getInspectionClass(): Class<out PyInspection> = PyInvalidEscapeSequenceInspection::class.java
|
||||
|
||||
fun testBasic() = doTest()
|
||||
|
||||
fun testBytes() = doTest()
|
||||
|
||||
fun testFstrings() = doTest()
|
||||
|
||||
fun testEscapeBackslash() {
|
||||
doQuickFixTest(PyPsiBundle.message("QFIX.escape.backslash"), "escapeBackslash_before.py", "escapeBackslash_after.py")
|
||||
}
|
||||
|
||||
fun testConvertToRawString() {
|
||||
doQuickFixTest(PyPsiBundle.message("QFIX.convert.to.raw.string"), "convertToRawString_before.py", "convertToRawString_after.py")
|
||||
}
|
||||
|
||||
fun testConvertToRawStringNotAvailable() {
|
||||
myFixture.configureByFile(testCaseDirectory + "convertToRawStringNotAvailable_before.py")
|
||||
myFixture.enableInspections(getInspectionClass())
|
||||
|
||||
val actions = myFixture.filterAvailableIntentions(PyPsiBundle.message("QFIX.convert.to.raw.string"))
|
||||
assertEmpty("Convert to raw string should not be available if string contains valid escapes", actions)
|
||||
|
||||
assertNotNull(myFixture.findSingleIntention(PyPsiBundle.message("QFIX.escape.backslash")))
|
||||
}
|
||||
|
||||
private fun doQuickFixTest(hint: String, sourceFile: String, expectedFile: String) {
|
||||
myFixture.configureByFile(testCaseDirectory + sourceFile)
|
||||
myFixture.enableInspections(getInspectionClass())
|
||||
|
||||
val action = myFixture.findSingleIntention(hint)
|
||||
myFixture.launchAction(action)
|
||||
myFixture.checkResultByFile(testCaseDirectory + expectedFile)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user