PY-79967 Support HTML injections inside template strings

GitOrigin-RevId: 435d956e12f08c2a51c1f772cdbfa91901135e4b
This commit is contained in:
Daniil Kalinin
2025-04-28 19:14:39 +00:00
committed by intellij-monorepo-bot
parent 9d86a1d671
commit 2647731608
9 changed files with 203 additions and 0 deletions
@@ -13,6 +13,11 @@
<place><![CDATA[pyLiteralExpression().and(pyArgument("path", 0))]]></place>
</injection>
<injection language="HTML" injector-id="python">
<display-name>HTML injections inside f-strings and t-strings</display-name>
<place><![CDATA[templateOrFormattedStringContainsHTML()]]></place>
</injection>
<injection language="SQL" injector-id="python">
<display-name>"SQL select/delete/insert/update/create"</display-name>
@@ -6,6 +6,7 @@ import com.intellij.patterns.InitialPatternCondition;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.util.ProcessingContext;
import com.jetbrains.python.ast.PyAstStringElement;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.documentation.docstrings.DocStringUtil;
import com.jetbrains.python.patterns.PyElementPattern;
@@ -21,6 +22,8 @@ import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import static com.jetbrains.python.codeInsight.template.PyHTMLInjectionControllerKt.looksLikeHTML;
/**
* Provides patterns for literals, strings, arguments and function/method arguments of Python.
* <p>
@@ -69,6 +72,19 @@ public final class PythonPatterns extends PlatformPatterns {
});
}
public static @NotNull PyElementPattern.Capture<PyFormattedStringElement> templateOrFormattedStringContainsHTML() {
return new PyElementPattern.Capture<>(PyFormattedStringElement.class) {
@Override
public boolean accepts(@Nullable Object o, ProcessingContext context) {
if (o instanceof PyStringLiteralExpression stringLiteralExpression && stringLiteralExpression.getStringElements().size() == 1) {
PyAstStringElement stringElement = stringLiteralExpression.getStringElements().get(0);
return stringElement.isTemplate() && looksLikeHTML(stringElement.getContent());
}
return false;
}
};
}
public static @NotNull PyElementPattern.Capture<PyExpression> pyModuleFunctionArgument(@Nullable String functionName,
int index,
@NotNull String moduleName) {
@@ -0,0 +1,29 @@
// 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.codeInsight.template
import com.intellij.xml.util.HtmlUtil
private val OBVIOUS_HTML_TAGS = setOf("html", "div", "span", "body", "p", "h1", "h2", "h3", "img",
"table", "ul", "ol", "li", "a", "img", "form", "script")
fun looksLikeHTML(content: String): Boolean {
if (content.isEmpty()) return false
val cleanContent = content.trim().replace(Regex("[\n\r]"), "")
if (cleanContent.startsWith("<")) {
val tagNamePattern = "<(\\w+)".toRegex()
val tagNameMatch = tagNamePattern.find(cleanContent)
if (tagNameMatch != null) {
val tagName = tagNameMatch.groupValues[1]
if (OBVIOUS_HTML_TAGS.contains(tagName) ||
HtmlUtil.isHtmlBlockTag(tagName, false) ||
HtmlUtil.isTagWithOptionalEnd(tagName, false)) {
return true
}
}
}
return false
}
@@ -0,0 +1,2 @@
<caret>name = "User"
greeting = t'Hi, {name}!'
@@ -0,0 +1,2 @@
username = "User"
greeting = t'Hi, {username}!'
@@ -0,0 +1,7 @@
<caret>name = "User"
greeting = t"""
<div>
<h2>Hi, {name}!</h2>
<p>This is a t-string with both HTML and Python interpolation.</p>
</div>
"""
@@ -0,0 +1,7 @@
username = "User"
greeting = t"""
<div>
<h2>Hi, {username}!</h2>
<p>This is a t-string with both HTML and Python interpolation.</p>
</div>
"""
@@ -0,0 +1,124 @@
// 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.codeInsight.template;
import com.intellij.lang.Language;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiLanguageInjectionHost;
import com.jetbrains.python.PythonFileType;
import com.jetbrains.python.fixtures.PyTestCase;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* Tests for HTML language injection in Python template strings (t-strings) and f-strings.
*/
public class PyHtmlTemplateInjectionTest extends PyTestCase {
public void testSimpleHtmlInjection() {
doTestHtmlInjected("x = t'<caret><html><body><h1>Hello, World!</h1></body></html>'");
}
public void testSimpleMultiLineHtmlInjection() {
doTestHtmlInjected("""
html_content = t'''
<caret><html>
<head>
<title>Test HTML Injection</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
'''
""");
}
public void testHtmlWithInterpolation() {
doTestHtmlInjected("""
name = "John"
x = t'<div><h2>Welcome,<caret> {name}!</h2></div>'
""");
}
public void testSingleQuotedTString() {
doTestHtmlInjected("x = t'<caret><span>This is a single-quoted t-string</span>'");
}
public void testRawTString() {
doTestHtmlInjected("x = tr'<caret><div class=\"raw\">Raw t-string with HTML</div>'");
}
public void testNoInjectionInRegularString() {
doTestNoInjection("x = \"<html><body><h1>Hello, World!</h1></body></html>\"");
}
public void testNoInjectionInFString() {
doTestNoInjection("x = f'<caret><span>This is an f-string</span>'");
}
public void testNoInjectionInNonHtmlTString() {
doTestNoInjection("x = t'<caret>plain string, nothing to see here'");
}
public void testNoInjectionInNonHtmlTStringWithInterpolation() {
doTestNoInjection("""
name = "John"
x = t'<caret>Hi, {name}!'
""");
}
public void testNoInjectionInEmptyTString() {
doTestNoInjection("x = t'<caret>'");
}
public void testNoInjectionInJsonTString() {
doTestNoInjection("x = t'<caret>{\"name\": \"John\", \"age\": 30}'");
}
public void testHtmlInjectedByScriptTag() {
doTestHtmlInjected("""
evil = t"<caret><script>alert('evil')</script>"
""");
}
public void testHtmlInjectedByImgTagWithAttributes() {
doTestHtmlInjected("x = t'<caret><img src=\"shrubbery.jpg\" alt=\"looks nice\" />'");
}
public void testHtmlInjectedByImgTagWithInterpolation() {
doTestHtmlInjected("""
attributes = {"src": "shrubbery.jpg", "alt": "looks nice"}
template = t"<caret><img {attributes} />"
""");
}
private void doTestNoInjection(@NotNull String text) {
myFixture.configureByText(PythonFileType.INSTANCE, text);
InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject());
PsiLanguageInjectionHost host = languageManager.getInjectionHost(getElementAtCaret());
assertNull(host);
}
private void doTestHtmlInjected(@NotNull String text) {
myFixture.configureByText(PythonFileType.INSTANCE, text);
InjectedLanguageManager languageManager = InjectedLanguageManager.getInstance(myFixture.getProject());
PsiElement elementAtCaret = getElementAtCaret();
assertNotNull("Element under caret not found", elementAtCaret);
PsiLanguageInjectionHost host = languageManager.getInjectionHost(elementAtCaret);
assertNotNull("Injection host is null", host);
List<Pair<PsiElement, TextRange>> injectedPsiFiles = languageManager.getInjectedPsiFiles(host);
assertNotNull("No injected PSI files found", injectedPsiFiles);
assertFalse("No injected elements found", injectedPsiFiles.isEmpty());
for (Pair<PsiElement, TextRange> pair : injectedPsiFiles) {
assertEquals(pair.first.getLanguage(), Language.findLanguageByID("HTML"));
}
}
}
@@ -10,6 +10,7 @@ import com.intellij.refactoring.BaseRefactoringProcessor;
import com.intellij.refactoring.util.TextOccurrencesUtil;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.util.IncorrectOperationException;
import com.jetbrains.python.PythonFileType;
import com.jetbrains.python.PythonTestUtil;
import com.jetbrains.python.documentation.docstrings.DocStringFormat;
import com.jetbrains.python.fixtures.PyTestCase;
@@ -413,6 +414,16 @@ public class PyRenameTest extends PyTestCase {
doTest("renamed");
}
// PY-79967
public void testRenameVariableInTStringWithHTMLInjection() {
doTest("username");
}
// PY-79967
public void testRenameVariableInSimpleTemplateString() {
doTest("username");
}
private void renameWithDocStringFormat(DocStringFormat format, final String newName) {
runWithDocStringFormat(format, () -> doTest(newName));
}