diff --git a/platform/lang-api/src/com/intellij/psi/util/PartiallyKnownString.kt b/platform/lang-api/src/com/intellij/psi/util/PartiallyKnownString.kt index fc9e8207499b..770c6ab1741c 100644 --- a/platform/lang-api/src/com/intellij/psi/util/PartiallyKnownString.kt +++ b/platform/lang-api/src/com/intellij/psi/util/PartiallyKnownString.kt @@ -33,6 +33,17 @@ class PartiallyKnownString(val segments: List) { 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 diff --git a/plugins/devkit/devkit-kotlin-tests/intellij.devkit.kotlin.tests.iml b/plugins/devkit/devkit-kotlin-tests/intellij.devkit.kotlin.tests.iml index 2ed3f7b6e981..89dd55bc86a6 100644 --- a/plugins/devkit/devkit-kotlin-tests/intellij.devkit.kotlin.tests.iml +++ b/plugins/devkit/devkit-kotlin-tests/intellij.devkit.kotlin.tests.iml @@ -12,5 +12,6 @@ + \ No newline at end of file diff --git a/plugins/devkit/devkit-kotlin-tests/testSrc/org/jetbrains/idea/devkit/kotlin/inspections/KtI18NInspectionTest.kt b/plugins/devkit/devkit-kotlin-tests/testSrc/org/jetbrains/idea/devkit/kotlin/inspections/KtI18NInspectionTest.kt new file mode 100644 index 000000000000..0a478ae1af2d --- /dev/null +++ b/plugins/devkit/devkit-kotlin-tests/testSrc/org/jetbrains/idea/devkit/kotlin/inspections/KtI18NInspectionTest.kt @@ -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("text") + foo("templated ${"\$"}s end") + foo("concatenated " + s + " end") + } + } + """.trimIndent()) + myFixture.testHighlighting() + } + +} + diff --git a/plugins/java-i18n/src/com/intellij/codeInspection/i18n/I18nInspection.java b/plugins/java-i18n/src/com/intellij/codeInspection/i18n/I18nInspection.java index 3d50c91bec26..351b930b1463 100644 --- a/plugins/java-i18n/src/com/intellij/codeInspection/i18n/I18nInspection.java +++ b/plugins/java-i18n/src/com/intellij/codeInspection/i18n/I18nInspection.java @@ -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 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 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 nonNlsTargets) { + private static boolean isInNonNlsEquals(UInjectionHost expression, final Set 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 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; diff --git a/uast/uast-common/src/org/jetbrains/uast/expressions/UStringConcatenationsFacade.kt b/uast/uast-common/src/org/jetbrains/uast/expressions/UStringConcatenationsFacade.kt index c0a952d1253c..b9750b185278 100644 --- a/uast/uast-common/src/org/jetbrains/uast/expressions/UStringConcatenationsFacade.kt +++ b/uast/uast-common/src/org/jetbrains/uast/expressions/UStringConcatenationsFacade.kt @@ -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) { - val uastOperands: Sequence = 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 = 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? = 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() ?: return null + return createFromUExpression(uElement) } } } \ No newline at end of file diff --git a/uast/uast-common/src/org/jetbrains/uast/expressions/uastLiteralUtils.kt b/uast/uast-common/src/org/jetbrains/uast/expressions/uastLiteralUtils.kt index af79c4eb9e4d..d113657affea 100644 --- a/uast/uast-common/src/org/jetbrains/uast/expressions/uastLiteralUtils.kt +++ b/uast/uast-common/src/org/jetbrains/uast/expressions/uastLiteralUtils.kt @@ -138,6 +138,13 @@ val UExpression.allPsiLanguageInjectionHosts: List 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. *