From 49f0880f575cc9c9b7d7a6f4349e1a004a8ce3cf Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Mon, 2 Sep 2024 15:21:00 +0200 Subject: [PATCH] [java] IDEA-358431 Support MessageFormat specifier-to-argument navigation, similar to String.format GitOrigin-RevId: 5bfb87b48e714f92f5c469d4110426ff76f8c14b --- .../IncorrectMessageFormatInspection.java | 12 +- .../src/com/siyeh/ig/format/FormatDecode.java | 24 ++++ .../siyeh/ig/format/FormatPlaceholder.java | 20 +++ .../message => format}/MessageFormatUtil.java | 36 +++++- .../StringFormatSymbolReferenceProvider.java | 120 +++++++++++++----- ...catenationArgumentToLogCallInspection.java | 2 +- .../MessageFormatUtilTest.java | 4 +- ...ringFormatSymbolReferenceProviderTest.java | 69 ++++++++-- ...itPropertiesMessageValidationInspection.kt | 6 +- 9 files changed, 228 insertions(+), 65 deletions(-) create mode 100644 java/java-analysis-impl/src/com/siyeh/ig/format/FormatPlaceholder.java rename java/java-analysis-impl/src/com/siyeh/ig/{bugs/message => format}/MessageFormatUtil.java (95%) rename java/java-tests/testSrc/com/siyeh/ig/{bugs/message => format}/MessageFormatUtilTest.java (99%) diff --git a/java/java-analysis-impl/src/com/siyeh/ig/bugs/IncorrectMessageFormatInspection.java b/java/java-analysis-impl/src/com/siyeh/ig/bugs/IncorrectMessageFormatInspection.java index 5615701dd5a7..afd5ffe1e552 100644 --- a/java/java-analysis-impl/src/com/siyeh/ig/bugs/IncorrectMessageFormatInspection.java +++ b/java/java-analysis-impl/src/com/siyeh/ig/bugs/IncorrectMessageFormatInspection.java @@ -11,8 +11,7 @@ import com.intellij.psi.impl.source.tree.java.PsiEmptyExpressionImpl; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.containers.ContainerUtil; import com.siyeh.InspectionGadgetsBundle; -import com.siyeh.ig.bugs.message.MessageFormatUtil; -import com.siyeh.ig.callMatcher.CallMatcher; +import com.siyeh.ig.format.MessageFormatUtil; import com.siyeh.ig.psiutils.ConstructionUtils; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NotNull; @@ -20,15 +19,8 @@ import org.jetbrains.annotations.Nullable; import java.util.*; -import static com.siyeh.ig.callMatcher.CallMatcher.anyOf; -import static com.siyeh.ig.callMatcher.CallMatcher.staticCall; - public final class IncorrectMessageFormatInspection extends AbstractBaseJavaLocalInspectionTool { - private static final CallMatcher PATTERN_METHODS = anyOf( - staticCall("java.text.MessageFormat", "format").parameterCount(2) - ); - @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { @@ -64,7 +56,7 @@ public final class IncorrectMessageFormatInspection extends AbstractBaseJavaLoca @Override public void visitMethodCallExpression(@NotNull PsiMethodCallExpression call) { - if (PATTERN_METHODS.test(call)) { + if (MessageFormatUtil.PATTERN_METHODS.test(call)) { List indexes = checkStringFormatAndGetIndexes(call.getArgumentList().getExpressions()[0]); if (indexes != null) { diff --git a/java/java-analysis-impl/src/com/siyeh/ig/format/FormatDecode.java b/java/java-analysis-impl/src/com/siyeh/ig/format/FormatDecode.java index 016609dcbf10..e95ccd688d79 100644 --- a/java/java-analysis-impl/src/com/siyeh/ig/format/FormatDecode.java +++ b/java/java-analysis-impl/src/com/siyeh/ig/format/FormatDecode.java @@ -38,6 +38,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.IntStream; +/** + * Utilities related to printf-like format string + */ public final class FormatDecode { private static final Pattern fsPattern = Pattern.compile( @@ -603,6 +606,27 @@ public final class FormatDecode { } } + /** + * @param validators validators returned from {@link #decode(String, int)} or similar methods + * @return list of {@link FormatPlaceholder} objects + */ + public static @NotNull List<@NotNull FormatPlaceholder> asPlaceholders(Validator @NotNull [] validators) { + List result = new ArrayList<>(); + for (int i = 0; i < validators.length; i++) { + FormatDecode.Validator metaValidator = validators[i]; + if (metaValidator == null) continue; + Collection unpacked = metaValidator instanceof FormatDecode.MultiValidator multi ? + multi.getValidators() : List.of(metaValidator); + for (FormatDecode.Validator validator : unpacked) { + TextRange stringRange = validator.getRange(); + if (stringRange == null) continue; + record MyPlaceholder(int index, @NotNull TextRange range) implements FormatPlaceholder {} + result.add(new MyPlaceholder(i, stringRange)); + } + } + return result; + } + public record Spec(@Nullable String posSpec , @Nullable String flags, @Nullable String width, diff --git a/java/java-analysis-impl/src/com/siyeh/ig/format/FormatPlaceholder.java b/java/java-analysis-impl/src/com/siyeh/ig/format/FormatPlaceholder.java new file mode 100644 index 000000000000..f066ae10558a --- /dev/null +++ b/java/java-analysis-impl/src/com/siyeh/ig/format/FormatPlaceholder.java @@ -0,0 +1,20 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.siyeh.ig.format; + +import com.intellij.openapi.util.TextRange; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a single placeholder in a format string, like %d in printf-format, or {1} in MessageFormat-format + */ +public interface FormatPlaceholder { + /** + * @return zero-based index of the argument, which corresponds to this format placeholder + */ + int index(); + + /** + * @return range inside the original format string which is occupied by a given placeholder + */ + @NotNull TextRange range(); +} diff --git a/java/java-analysis-impl/src/com/siyeh/ig/bugs/message/MessageFormatUtil.java b/java/java-analysis-impl/src/com/siyeh/ig/format/MessageFormatUtil.java similarity index 95% rename from java/java-analysis-impl/src/com/siyeh/ig/bugs/message/MessageFormatUtil.java rename to java/java-analysis-impl/src/com/siyeh/ig/format/MessageFormatUtil.java index 97137d0921f3..9ca642c89ddd 100644 --- a/java/java-analysis-impl/src/com/siyeh/ig/bugs/message/MessageFormatUtil.java +++ b/java/java-analysis-impl/src/com/siyeh/ig/format/MessageFormatUtil.java @@ -1,11 +1,11 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.siyeh.ig.bugs.message; +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.siyeh.ig.format; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.util.containers.ContainerUtil; +import com.siyeh.ig.callMatcher.CallMatcher; import it.unimi.dsi.fastutil.ints.Int2IntFunction; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; @@ -13,7 +13,19 @@ import org.jetbrains.annotations.VisibleForTesting; import java.text.ChoiceFormat; import java.util.*; +import static com.siyeh.ig.callMatcher.CallMatcher.anyOf; +import static com.siyeh.ig.callMatcher.CallMatcher.staticCall; + +/** + * Utilities related to MessageFormat-like format string + */ public final class MessageFormatUtil { + /** + * Matcher to match known JDK library methods that accept MessageFormat-like format string, along with an array/vararg of arguments + */ + public static final CallMatcher PATTERN_METHODS = anyOf( + staticCall("java.text.MessageFormat", "format").parameterCount(2) + ); private static final Map> knownContractions = Map.ofEntries( Map.entry("aren", List.of("t")), @@ -46,7 +58,10 @@ public final class MessageFormatUtil { Map.entry("you", List.of("d", "ll", "re", "ve")) ); - @ApiStatus.Experimental + /** + * @param pattern MessageFormat-like formatting string + * @return MessageFormatResult object that contains information about placeholders and possible syntax errors inside the pattern + */ @NotNull public static MessageFormatResult checkFormat(@NotNull String pattern) { if (pattern.isEmpty()) { @@ -255,7 +270,7 @@ public final class MessageFormatUtil { MessageFormatPart part = holder.getLastPart(); if (!(part.getMessageFormatElement() != null && part.getMessageFormatElement().currentPart == MessageFormatElementPart.FORMAT_TYPE && - part.getMessageFormatElement().formatTypeSegment.length() == 0)) { + part.getMessageFormatElement().formatTypeSegment.isEmpty())) { holder.addChar(ch); } } @@ -323,7 +338,7 @@ public final class MessageFormatUtil { MessageHolder holder = parseMessageHolder(nextPattern); if (holder.errors.isEmpty()) { List notStrings = - ContainerUtil.filter(holder.parts, t -> !(t.getParsedType() == MessageFormatParsedType.STRING && t.getText().length() == 0)); + ContainerUtil.filter(holder.parts, t -> !(t.getParsedType() == MessageFormatParsedType.STRING && t.getText().isEmpty())); if (notStrings.size() == 1 && notStrings.get(0).getParsedType() == MessageFormatParsedType.FORMAT_ELEMENT) { return nextQuote + current; } @@ -445,11 +460,18 @@ public final class MessageFormatUtil { RUNTIME_EXCEPTION, WARNING, WEAK_WARNING } + /** + * Information about MessageFormat-like format string + * + * @param valid if true, then the format string is valid + * @param errors list of errors inside the format string + * @param placeholders list of placeholders inside the format string + */ public record MessageFormatResult(boolean valid, @NotNull List errors, @NotNull List placeholders) { } - public record MessageFormatPlaceholder(int index, @NotNull TextRange range, boolean isString) { + public record MessageFormatPlaceholder(int index, @NotNull TextRange range, boolean isString) implements FormatPlaceholder { } static class MessageFormatPart { diff --git a/java/java-impl/src/com/siyeh/ig/format/StringFormatSymbolReferenceProvider.java b/java/java-impl/src/com/siyeh/ig/format/StringFormatSymbolReferenceProvider.java index 018dfe2c76e4..dde27e041fc9 100644 --- a/java/java-impl/src/com/siyeh/ig/format/StringFormatSymbolReferenceProvider.java +++ b/java/java-impl/src/com/siyeh/ig/format/StringFormatSymbolReferenceProvider.java @@ -21,16 +21,18 @@ import com.intellij.psi.*; import com.intellij.psi.search.LocalSearchScope; import com.intellij.psi.search.SearchScope; import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.psi.util.PsiUtil; import com.intellij.util.ObjectUtils; +import com.intellij.util.containers.ContainerUtil; import com.siyeh.ig.psiutils.ExpressionUtils; +import com.siyeh.ig.psiutils.MethodCallUtils; +import com.siyeh.ig.psiutils.VariableAccessUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Objects; +import java.util.*; +import java.util.function.Supplier; public final class StringFormatSymbolReferenceProvider implements PsiSymbolReferenceProvider { @Override @@ -44,12 +46,30 @@ public final class StringFormatSymbolReferenceProvider implements PsiSymbolRefer static @NotNull List<@NotNull PsiSymbolReference> getReferences(@NotNull PsiLiteralExpression expression) { PsiCallExpression callExpression = findContextCall(expression); if (callExpression == null) return List.of(); + List<@NotNull PsiSymbolReference> refs = getPrintFormatRefs(expression, callExpression); + return refs.isEmpty() ? getMessageFormatRefs(expression, callExpression) : refs; + } + + private static @NotNull List<@NotNull PsiSymbolReference> getMessageFormatRefs(@NotNull PsiLiteralExpression expression, + @NotNull PsiCallExpression callExpression) { + if (!MessageFormatUtil.PATTERN_METHODS.matches(callExpression)) return List.of(); + String formatString = ObjectUtils.tryCast(expression.getValue(), String.class); + if (formatString == null) return List.of(); + MessageFormatUtil.MessageFormatResult format = MessageFormatUtil.checkFormat(formatString); + List placeholders = format.placeholders(); + if (placeholders.isEmpty()) return List.of(); + return createReferences(callExpression, 0, expression, placeholders); + } + + private static @NotNull List<@NotNull PsiSymbolReference> getPrintFormatRefs(@NotNull PsiLiteralExpression expression, + @NotNull PsiCallExpression callExpression) { FormatDecode.FormatArgument argument = FormatDecode.FormatArgument.extract(callExpression, List.of(), List.of(), true); - if (argument == null || !PsiTreeUtil.isAncestor(argument.getExpression(), expression, false)) return List.of(); + if (argument == null || !PsiTreeUtil.isAncestor(resolve(argument.getExpression()), expression, false)) return List.of(); String formatString = ObjectUtils.tryCast(expression.getValue(), String.class); if (formatString == null) return List.of(); PsiExpression[] arguments = Objects.requireNonNull(callExpression.getArgumentList()).getExpressions(); - int argumentCount = arguments.length - argument.getIndex(); + int index = argument.getIndex(); + int argumentCount = arguments.length - index; FormatDecode.Validator[] validators; try { validators = FormatDecode.decodeNoVerify(formatString, argumentCount); @@ -57,27 +77,43 @@ public final class StringFormatSymbolReferenceProvider implements PsiSymbolRefer catch (FormatDecode.IllegalFormatException e) { return List.of(); } - List result = new ArrayList<>(); - for (int i = 0; i < validators.length; i++) { - int index = argument.getIndex() + i; - if (index >= arguments.length) break; + List<@NotNull FormatPlaceholder> placeholders = FormatDecode.asPlaceholders(validators); + return createReferences(callExpression, index - 1, expression, placeholders); + } - FormatDecode.Validator metaValidator = validators[i]; - if (metaValidator == null) continue; - Collection unpacked = metaValidator instanceof FormatDecode.MultiValidator multi ? - multi.getValidators() : List.of(metaValidator); - for (FormatDecode.Validator validator : unpacked) { - TextRange stringRange = validator.getRange(); - if (stringRange == null) continue; - TextRange range = ExpressionUtils.findStringLiteralRange(expression, stringRange.getStartOffset(), - stringRange.getEndOffset()); - if (range == null) continue; - result.add(new JavaFormatArgumentSymbolReference(expression, range, arguments[index])); - } + private static @NotNull List<@NotNull PsiSymbolReference> createReferences(@NotNull PsiCallExpression callExpression, + int formatStringIndex, + @NotNull PsiLiteralExpression formatExpression, + @NotNull List placeholders) { + List formatArguments = getFormatArguments(callExpression, formatStringIndex); + List<@NotNull PsiSymbolReference> result = new ArrayList<>(); + for (FormatPlaceholder placeholder : placeholders) { + int index = placeholder.index(); + if (index >= formatArguments.size()) continue; + TextRange stringRange = placeholder.range(); + TextRange range = ExpressionUtils.findStringLiteralRange(formatExpression, stringRange.getStartOffset(), + stringRange.getEndOffset()); + if (range == null) continue; + PsiExpression arg = formatArguments.get(index); + result.add(new JavaFormatArgumentSymbolReference(formatExpression, range, () -> new JavaFormatArgumentSymbol(arg, formatStringIndex))); } return result; } + private static List getFormatArguments(@NotNull PsiCallExpression callExpression, int formatIndex) { + PsiExpression[] arguments = Objects.requireNonNull(callExpression.getArgumentList()).getExpressions(); + int firstArgument = formatIndex + 1; + if (arguments.length <= firstArgument) return List.of(); + if (MethodCallUtils.isVarArgCall(callExpression)) { + return Arrays.asList(arguments).subList(firstArgument, arguments.length); + } + if (arguments.length != firstArgument + 1) return List.of(); + if (!(PsiUtil.skipParenthesizedExprDown(arguments[firstArgument]) instanceof PsiNewExpression array)) return List.of(); + PsiArrayInitializerExpression initializer = array.getArrayInitializer(); + if (initializer == null) return List.of(); + return Arrays.asList(initializer.getInitializers()); + } + private static boolean hintsCheck(@NotNull PsiSymbolReferenceHints hints) { if (!hints.getReferenceClass().isAssignableFrom(JavaFormatArgumentSymbolReference.class)) return false; Class targetClass = hints.getTargetClass(); @@ -89,6 +125,12 @@ public final class StringFormatSymbolReferenceProvider implements PsiSymbolRefer private static PsiCallExpression findContextCall(PsiElement context) { if (!(context instanceof PsiExpression expr)) return null; expr = ExpressionUtils.getPassThroughExpression(expr); + if (expr.getParent() instanceof PsiLocalVariable variable) { + PsiReferenceExpression ref = ContainerUtil.getOnlyItem(VariableAccessUtils.getVariableReferences(variable)); + if (ref != null) { + expr = ref; + } + } if (expr.getParent() instanceof PsiExpressionList list && list.getParent() instanceof PsiCallExpression call) { return call; } @@ -103,12 +145,14 @@ public final class StringFormatSymbolReferenceProvider implements PsiSymbolRefer private static class JavaFormatArgumentSymbolReference implements PsiSymbolReference { private final PsiExpression myFormat; private final TextRange myRange; - private final PsiExpression myArgument; + private final Supplier myResolver; - private JavaFormatArgumentSymbolReference(@NotNull PsiExpression format, @NotNull TextRange range, @NotNull PsiExpression argument) { + private JavaFormatArgumentSymbolReference(@NotNull PsiExpression format, + @NotNull TextRange range, + @NotNull Supplier resolver) { myFormat = format; myRange = range; - myArgument = argument; + myResolver = resolver; } @Override @@ -123,21 +167,24 @@ public final class StringFormatSymbolReferenceProvider implements PsiSymbolRefer @Override public @NotNull Collection resolveReference() { - return List.of(new JavaFormatArgumentSymbol(myArgument)); + return List.of(myResolver.get()); } } @ApiStatus.Internal public static final class JavaFormatArgumentSymbol implements Symbol, SearchTarget, NavigatableSymbol { private final @NotNull PsiExpression myExpression; + private final int myFormatStringIndex; - JavaFormatArgumentSymbol(@NotNull PsiExpression argument) { + JavaFormatArgumentSymbol(@NotNull PsiExpression argument, int index) { myExpression = argument; + myFormatStringIndex = index; } @Override public @NotNull Pointer createPointer() { - return Pointer.delegatingPointer(SmartPointerManager.createPointer(myExpression), JavaFormatArgumentSymbol::new); + return Pointer.delegatingPointer(SmartPointerManager.createPointer(myExpression), + argument -> new JavaFormatArgumentSymbol(argument, myFormatStringIndex)); } @Override @@ -158,9 +205,13 @@ public final class StringFormatSymbolReferenceProvider implements PsiSymbolRefer !(list.getParent() instanceof PsiCallExpression call)) { return null; } - FormatDecode.FormatArgument argument = FormatDecode.FormatArgument.extract(call, List.of(), List.of(), true); - if (argument == null) return null; - return argument.getExpression(); + PsiExpressionList argumentList = call.getArgumentList(); + if (argumentList == null) return null; + PsiExpression[] expressions = argumentList.getExpressions(); + if (expressions.length <= myFormatStringIndex) { + return null; + } + return resolve(expressions[myFormatStringIndex]); } @Override @@ -191,4 +242,11 @@ public final class StringFormatSymbolReferenceProvider implements PsiSymbolRefer return myExpression.hashCode(); } } + + private static @Nullable PsiExpression resolve(PsiExpression target) { + if (target instanceof PsiReferenceExpression ref && ref.resolve() instanceof PsiLocalVariable local) { + return local.getInitializer(); + } + return target; + } } diff --git a/java/java-impl/src/com/siyeh/ig/logging/StringConcatenationArgumentToLogCallInspection.java b/java/java-impl/src/com/siyeh/ig/logging/StringConcatenationArgumentToLogCallInspection.java index 02d228de0ef4..9778160b6fbe 100644 --- a/java/java-impl/src/com/siyeh/ig/logging/StringConcatenationArgumentToLogCallInspection.java +++ b/java/java-impl/src/com/siyeh/ig/logging/StringConcatenationArgumentToLogCallInspection.java @@ -19,9 +19,9 @@ import com.siyeh.InspectionGadgetsBundle; import com.siyeh.ig.BaseInspection; import com.siyeh.ig.BaseInspectionVisitor; import com.siyeh.ig.PsiReplacementUtil; -import com.siyeh.ig.bugs.message.MessageFormatUtil; import com.siyeh.ig.callMatcher.CallMatcher; import com.siyeh.ig.format.FormatDecode; +import com.siyeh.ig.format.MessageFormatUtil; import com.siyeh.ig.psiutils.CommentTracker; import com.siyeh.ig.psiutils.ExpressionUtils; import com.siyeh.ig.psiutils.TypeUtils; diff --git a/java/java-tests/testSrc/com/siyeh/ig/bugs/message/MessageFormatUtilTest.java b/java/java-tests/testSrc/com/siyeh/ig/format/MessageFormatUtilTest.java similarity index 99% rename from java/java-tests/testSrc/com/siyeh/ig/bugs/message/MessageFormatUtilTest.java rename to java/java-tests/testSrc/com/siyeh/ig/format/MessageFormatUtilTest.java index 8dbf21f8b960..3b350ff3e86a 100644 --- a/java/java-tests/testSrc/com/siyeh/ig/bugs/message/MessageFormatUtilTest.java +++ b/java/java-tests/testSrc/com/siyeh/ig/format/MessageFormatUtilTest.java @@ -1,5 +1,5 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -package com.siyeh.ig.bugs.message; +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.siyeh.ig.format; import com.intellij.util.containers.ContainerUtil; import org.junit.Assert; diff --git a/java/java-tests/testSrc/com/siyeh/ig/format/StringFormatSymbolReferenceProviderTest.java b/java/java-tests/testSrc/com/siyeh/ig/format/StringFormatSymbolReferenceProviderTest.java index 713b81f37e07..abf4e03a0898 100644 --- a/java/java-tests/testSrc/com/siyeh/ig/format/StringFormatSymbolReferenceProviderTest.java +++ b/java/java-tests/testSrc/com/siyeh/ig/format/StringFormatSymbolReferenceProviderTest.java @@ -4,7 +4,7 @@ package com.siyeh.ig.format; import com.intellij.model.Symbol; import com.intellij.model.psi.PsiSymbolReference; import com.intellij.model.psi.PsiSymbolReferenceService; -import com.intellij.psi.PsiConditionalExpression; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiLiteralExpression; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.testFramework.LightProjectDescriptor; @@ -24,28 +24,75 @@ public class StringFormatSymbolReferenceProviderTest extends LightJavaCodeInsigh public void testResolveFormatSpecifiers() { myFixture.configureByText("Test.java", """ - class Demo { + final class Demo { static void process(String s, Object date, boolean b) { String conditional = String.format(b ? "myFormat: num = %1$d, date = %2$s" : "myFormat: date = %2$s; num = %1$d", 123, date); } }"""); + PsiLiteralExpression str = getLiteral(); + Collection refs = PsiSymbolReferenceService.getService().getReferences(str); + assertEquals(2, refs.size()); + checkRefs(refs, str, str.getParent(), Map.of("%2$s", "date", "%1$d", "123")); + } + + public void testResolveFromLocalVar() { + myFixture.configureByText("Test.java", """ + final class Demo { + static void process(String s) { + String template = "Hello %d %s"; + System.out.printf(template, 123, s); + } + }"""); + PsiLiteralExpression str = getLiteral(); + Collection refs = PsiSymbolReferenceService.getService().getReferences(str); + assertEquals(2, refs.size()); + Map expected = Map.of("%d", "123", "%s", "s"); + checkRefs(refs, str, str, expected); + } + + public void testMessageFormat() { + myFixture.configureByText("Test.java", """ + import java.text.MessageFormat; + + final class Demo { + static void process(String s) { + String template = "Hello {1} {1} {0}"; + System.out.println(MessageFormat.format(template, s, 123)); + } + }"""); + PsiLiteralExpression str = getLiteral(); + Collection refs = PsiSymbolReferenceService.getService().getReferences(str); + assertEquals(3, refs.size()); + Map expected = Map.of("{0}", "s", "{1}", "123"); + checkRefs(refs, str, str, expected); + } + + private @NotNull PsiLiteralExpression getLiteral() { PsiLiteralExpression str = PsiTreeUtil.getParentOfType(myFixture.getFile().findElementAt(myFixture.getEditor().getCaretModel().getOffset()), PsiLiteralExpression.class); assertNotNull(str); - Collection refs = PsiSymbolReferenceService.getService().getReferences(str); - assertEquals(2, refs.size()); - Map expected = Map.of("%2$s", "date", "%1$d", "123"); + return str; + } + + private static @NotNull JavaFormatArgumentSymbol resolveRef(@NotNull PsiSymbolReference ref) { + Collection symbols = ref.resolveReference(); + assertEquals(1, symbols.size()); + Symbol symbol = CollectionUtils.getOnlyElement(symbols); + assertTrue(symbol instanceof JavaFormatArgumentSymbol); + return (JavaFormatArgumentSymbol)symbol; + } + + private static void checkRefs(@NotNull Collection refs, + @NotNull PsiLiteralExpression str, + @NotNull PsiElement formatString, + @NotNull Map expected) { for (PsiSymbolReference ref : refs) { assertEquals(str, ref.getElement()); String formatSpecifier = ref.getRangeInElement().substring(ref.getElement().getText()); - Collection symbols = ref.resolveReference(); - assertEquals(1, symbols.size()); - Symbol symbol = CollectionUtils.getOnlyElement(symbols); - assertTrue(symbol instanceof JavaFormatArgumentSymbol); - JavaFormatArgumentSymbol formatSymbol = (JavaFormatArgumentSymbol)symbol; - assertTrue(formatSymbol.getFormatString() instanceof PsiConditionalExpression); + JavaFormatArgumentSymbol formatSymbol = resolveRef(ref); + assertEquals(formatString, formatSymbol.getFormatString()); String expressionText = formatSymbol.getExpression().getText(); assertEquals(expected.get(formatSpecifier), expressionText); } diff --git a/plugins/devkit/intellij.devkit.i18n/src/DevKitPropertiesMessageValidationInspection.kt b/plugins/devkit/intellij.devkit.i18n/src/DevKitPropertiesMessageValidationInspection.kt index 97fb6d6233b5..b17db5c0afc2 100644 --- a/plugins/devkit/intellij.devkit.i18n/src/DevKitPropertiesMessageValidationInspection.kt +++ b/plugins/devkit/intellij.devkit.i18n/src/DevKitPropertiesMessageValidationInspection.kt @@ -9,9 +9,9 @@ import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.siyeh.ig.bugs.IncorrectMessageFormatInspection -import com.siyeh.ig.bugs.message.MessageFormatUtil -import com.siyeh.ig.bugs.message.MessageFormatUtil.MessageFormatError -import com.siyeh.ig.bugs.message.MessageFormatUtil.MessageFormatErrorType +import com.siyeh.ig.format.MessageFormatUtil +import com.siyeh.ig.format.MessageFormatUtil.MessageFormatError +import com.siyeh.ig.format.MessageFormatUtil.MessageFormatErrorType import org.jetbrains.idea.devkit.inspections.DevKitInspectionUtil private val SKIPPED_ERROR_TYPES: Set = setOf(MessageFormatErrorType.INDEX_NEGATIVE,