I18nInspection: Kotlin interpolated strings support (IDEA-234780)

GitOrigin-RevId: 1f9e01a0740fe1ba969db9af40a04651ebcfb8fe
This commit is contained in:
Nicolay Mitropolsky
2020-03-06 20:13:48 +03:00
committed by intellij-monorepo-bot
parent 6a6f45b597
commit 3c6c5ad37a
6 changed files with 105 additions and 35 deletions

View File

@@ -33,6 +33,17 @@ class PartiallyKnownString(val segments: List<StringEntry>) {
return stringBuffer.toString()
}
val concatenationOfKnown: String
get() {
(segments.singleOrNull() as? StringEntry.Known)?.let { return it.value }
val stringBuffer = StringBuffer()
for (segment in segments) {
if (segment is StringEntry.Known) stringBuffer.append(segment.value)
}
return stringBuffer.toString()
}
override fun toString(): String = segments.joinToString { segment ->
when (segment) {
is StringEntry.Known -> segment.value

View File

@@ -12,5 +12,6 @@
<orderEntry type="library" scope="RUNTIME" name="KotlinPlugin" level="project" />
<orderEntry type="library" scope="TEST" name="kotlin-stdlib-jdk8" level="project" />
<orderEntry type="module" module-name="intellij.devkit.java.tests" scope="TEST" />
<orderEntry type="module" module-name="intellij.java.i18n" scope="TEST" />
</component>
</module>

View File

@@ -0,0 +1,27 @@
// Copyright 2000-2020 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 org.jetbrains.idea.devkit.kotlin.inspections
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
class KtI18NInspectionTest : LightJavaCodeInsightFixtureTestCase() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(com.intellij.codeInspection.i18n.I18nInspection())
}
fun testFunctionParameters() {
myFixture.configureByText("Foo.kt", """
class Foo {
fun foo(s: String) {
foo(<warning descr="Hardcoded string literal: \"text\"">"text"</warning>)
foo(<warning descr="Hardcoded string literal: \"templated ${"\$"}s end\"">"templated ${"\$"}s end"</warning>)
foo(<warning descr="Hardcoded string literal: \"concatenated \"">"concatenated "</warning> + s + <warning descr="Hardcoded string literal: \" end\"">" end"</warning>)
}
}
""".trimIndent())
myFixture.testHighlighting()
}
}

View File

@@ -40,6 +40,8 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import org.jetbrains.uast.*;
import org.jetbrains.uast.expressions.UInjectionHost;
import org.jetbrains.uast.expressions.UStringConcatenationsFacade;
import org.jetbrains.uast.util.UastExpressionUtils;
import javax.swing.*;
@@ -497,16 +499,16 @@ public class I18nInspection extends AbstractBaseUastLocalInspectionTool implemen
public void visitElement(@NotNull PsiElement element) {
super.visitElement(element);
if (element instanceof PsiMember && ((PsiMember)element).getName() != null ||
if (element instanceof PsiMember && ((PsiMember)element).getName() != null ||
element instanceof PsiClassInitializer) {
return;
}
UElement uElement =
UastContextKt.toUElementOfExpectedTypes(element, ULiteralExpression.class, UAnnotation.class);
UastContextKt.toUElementOfExpectedTypes(element, UInjectionHost.class, UAnnotation.class);
if (uElement instanceof ULiteralExpression) {
visitLiteralExpression(element, (ULiteralExpression)uElement);
if (uElement instanceof UInjectionHost) {
visitLiteralExpression(element, (UInjectionHost)uElement);
return;
}
@@ -519,13 +521,11 @@ public class I18nInspection extends AbstractBaseUastLocalInspectionTool implemen
element.acceptChildren(this);
}
private void visitLiteralExpression(@NotNull PsiElement sourcePsi,
@NotNull ULiteralExpression expression) {
Object value = expression.getValue();
if (!(value instanceof String)) return;
String stringValue = (String)value;
if (stringValue.trim().isEmpty()) {
@NotNull UInjectionHost expression) {
String stringValue = getStringValueOfKnownPart(expression);
if (StringUtil.isEmptyOrSpaces(stringValue)) {
return;
}
@@ -589,8 +589,18 @@ public class I18nInspection extends AbstractBaseUastLocalInspectionTool implemen
}
}
private static String getStringValueOfKnownPart(@NotNull UInjectionHost expression) {
UStringConcatenationsFacade concatenationsFacade = UStringConcatenationsFacade.createFromUExpression(expression);
if (concatenationsFacade != null) {
return concatenationsFacade.asPartiallyKnownString().getConcatenationOfKnown();
}
else {
return expression.evaluateToString();
}
}
private boolean canBeI18ned(@NotNull Project project,
@NotNull ULiteralExpression expression,
@NotNull UInjectionHost expression,
@NotNull String value,
@NotNull Set<? super PsiModifierListOwner> nonNlsTargets) {
if (ignoreForNonAlpha && !StringUtil.containsAlphaCharacters(value)) {
@@ -672,7 +682,7 @@ public class I18nInspection extends AbstractBaseUastLocalInspectionTool implemen
return true;
}
private static boolean isArgOfEnumConstant(ULiteralExpression expression) {
private static boolean isArgOfEnumConstant(UInjectionHost expression) {
return expression.getUastParent() instanceof UEnumConstant;
}
@@ -680,8 +690,8 @@ public class I18nInspection extends AbstractBaseUastLocalInspectionTool implemen
myCachedNonNlsPattern = nonNlsCommentPattern.trim().isEmpty() ? null : Pattern.compile(nonNlsCommentPattern);
}
private static boolean isClassRef(final ULiteralExpression expression, String value) {
if (StringUtil.startsWithChar(value,'#')) {
private static boolean isClassRef(final UInjectionHost expression, String value) {
if (StringUtil.startsWithChar(value, '#')) {
value = value.substring(1); // A favor for JetBrains team to catch common Logger usage practice.
}
@@ -705,7 +715,7 @@ public class I18nInspection extends AbstractBaseUastLocalInspectionTool implemen
|| isPackageNonNls(psiPackage.getParentPackage());
}
private boolean isPassedToNonNlsVariable(@NotNull ULiteralExpression expression,
private boolean isPassedToNonNlsVariable(@NotNull UInjectionHost expression,
final Set<? super PsiModifierListOwner> nonNlsTargets) {
UExpression toplevel = JavaI18nUtil.getTopLevelExpression(expression);
PsiModifierListOwner var = null;
@@ -773,7 +783,7 @@ public class I18nInspection extends AbstractBaseUastLocalInspectionTool implemen
return AnnotationUtil.isAnnotated(parent, AnnotationUtil.NON_NLS, CHECK_EXTERNAL);
}
private static boolean isInNonNlsEquals(ULiteralExpression expression, final Set<? super PsiModifierListOwner> nonNlsTargets) {
private static boolean isInNonNlsEquals(UInjectionHost expression, final Set<? super PsiModifierListOwner> nonNlsTargets) {
UElement parent = UastUtils.skipParenthesizedExprUp(expression.getUastParent());
if (!(parent instanceof UQualifiedReferenceExpression)) return false;
UExpression selector = ((UQualifiedReferenceExpression)parent).getSelector();
@@ -845,7 +855,7 @@ public class I18nInspection extends AbstractBaseUastLocalInspectionTool implemen
return false;
}
private static boolean isReturnedFromAnnotatedMethod(final ULiteralExpression expression,
private static boolean isReturnedFromAnnotatedMethod(final UInjectionHost expression,
final String fqn,
final @Nullable Set<? super PsiModifierListOwner> nonNlsTargets) {
PsiMethod method;
@@ -902,7 +912,7 @@ public class I18nInspection extends AbstractBaseUastLocalInspectionTool implemen
return false;
}
private static boolean isToString(final ULiteralExpression expression) {
private static boolean isToString(final UInjectionHost expression) {
final UMethod method = UastUtils.getParentOfType(expression, UMethod.class);
if (method == null) return false;
final PsiType returnType = method.getReturnType();
@@ -912,7 +922,7 @@ public class I18nInspection extends AbstractBaseUastLocalInspectionTool implemen
&& "java.lang.String".equals(returnType.getCanonicalText());
}
private static boolean isArgOfJUnitAssertion(ULiteralExpression expression) {
private static boolean isArgOfJUnitAssertion(UInjectionHost expression) {
final UElement parent = UastUtils.skipParenthesizedExprUp(expression.getUastParent());
if (parent == null || !UastExpressionUtils.isMethodCall(parent)) {
return false;
@@ -938,7 +948,7 @@ public class I18nInspection extends AbstractBaseUastLocalInspectionTool implemen
InheritanceUtil.isInheritor(containingClass, "junit.framework.Assert");
}
private static boolean isArgOfSpecifiedExceptionConstructor(ULiteralExpression expression,
private static boolean isArgOfSpecifiedExceptionConstructor(UInjectionHost expression,
String[] specifiedExceptions) {
if (specifiedExceptions.length == 0) return false;

View File

@@ -16,18 +16,16 @@ import org.jetbrains.uast.*
*
* @see PartiallyKnownString
*/
class UStringConcatenationsFacade @ApiStatus.Experimental constructor(uContext: UExpression) {
class UStringConcatenationsFacade private constructor(private val uContext: UExpression, val uastOperands: Sequence<UExpression>) {
val uastOperands: Sequence<UExpression> = run {
when {
uContext is UPolyadicExpression -> uContext.operands.asSequence()
uContext is ULiteralExpression && uContext.uastParent !is UPolyadicExpression -> {
val host = uContext.sourceInjectionHost
if (host == null || !host.isValidHost) emptySequence() else sequenceOf(uContext)
}
else -> emptySequence()
}
}
@Deprecated("use factory method `UStringConcatenationsFacade.createFromUExpression`",
ReplaceWith("UStringConcatenationsFacade.createFromUExpression"))
@ApiStatus.Experimental
constructor(uContext: UExpression) : this(uContext, buildLazyUastOperands(uContext) ?: emptySequence())
@get:ApiStatus.Experimental
val rootUExpression: UExpression
get() = uContext
val psiLanguageInjectionHosts: Sequence<PsiLanguageInjectionHost> =
uastOperands.mapNotNull { (it as? ULiteralExpression)?.psiLanguageInjectionHost }.distinct()
@@ -101,13 +99,29 @@ class UStringConcatenationsFacade @ApiStatus.Experimental constructor(uContext:
}
companion object {
private fun buildLazyUastOperands(uContext: UExpression?): Sequence<UExpression>? = when {
uContext is UPolyadicExpression && isConcatenation(uContext) -> uContext.operands.asSequence()
uContext is ULiteralExpression && !isConcatenation(uContext.uastParent) -> {
val host = uContext.sourceInjectionHost
if (host == null || !host.isValidHost) emptySequence() else sequenceOf(uContext)
}
else -> null
}
@JvmStatic
fun create(context: PsiElement): UStringConcatenationsFacade? {
if (context !is PsiLanguageInjectionHost && context.firstChild !is PsiLanguageInjectionHost) {
fun createFromUExpression(uContext: UExpression?): UStringConcatenationsFacade? {
val operands = buildLazyUastOperands(uContext) ?: return null
return UStringConcatenationsFacade(uContext!!, operands)
}
@JvmStatic
fun create(context: PsiElement?): UStringConcatenationsFacade? {
if (context == null || context !is PsiLanguageInjectionHost && context.firstChild !is PsiLanguageInjectionHost) {
return null
}
val uElement = context.toUElement(UExpression::class.java) ?: return null
return UStringConcatenationsFacade(uElement)
val uElement = context.toUElementOfType<UExpression>() ?: return null
return createFromUExpression(uElement)
}
}
}

View File

@@ -138,6 +138,13 @@ val UExpression.allPsiLanguageInjectionHosts: List<PsiLanguageInjectionHost>
return emptyList()
}
@ApiStatus.Experimental
fun isConcatenation(uExpression: UElement?): Boolean {
if (uExpression !is UPolyadicExpression) return false
if (uExpression.operator != UastBinaryOperator.PLUS) return false
return true
}
/**
* @return a non-strict parent [PsiLanguageInjectionHost] for [sourcePsi] of given literal expression if it exists.
*