mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-15 02:59:33 +07:00
[java] IDEA-358431 Support MessageFormat specifier-to-argument navigation, similar to String.format
GitOrigin-RevId: 5bfb87b48e714f92f5c469d4110426ff76f8c14b
This commit is contained in:
committed by
intellij-monorepo-bot
parent
3b834aa61a
commit
49f0880f57
@@ -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<MessageFormatUtil.MessageFormatPlaceholder> indexes =
|
||||
checkStringFormatAndGetIndexes(call.getArgumentList().getExpressions()[0]);
|
||||
if (indexes != null) {
|
||||
|
||||
@@ -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<FormatPlaceholder> result = new ArrayList<>();
|
||||
for (int i = 0; i < validators.length; i++) {
|
||||
FormatDecode.Validator metaValidator = validators[i];
|
||||
if (metaValidator == null) continue;
|
||||
Collection<FormatDecode.Validator> 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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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<String, List<String>> 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<MessageFormatPart> 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<MessageFormatError> errors,
|
||||
@NotNull List<MessageFormatPlaceholder> 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 {
|
||||
@@ -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<MessageFormatUtil.MessageFormatPlaceholder> 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<PsiSymbolReference> 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<FormatDecode.Validator> 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<? extends FormatPlaceholder> placeholders) {
|
||||
List<PsiExpression> 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<PsiExpression> 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<? extends Symbol> 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<JavaFormatArgumentSymbol> myResolver;
|
||||
|
||||
private JavaFormatArgumentSymbolReference(@NotNull PsiExpression format, @NotNull TextRange range, @NotNull PsiExpression argument) {
|
||||
private JavaFormatArgumentSymbolReference(@NotNull PsiExpression format,
|
||||
@NotNull TextRange range,
|
||||
@NotNull Supplier<JavaFormatArgumentSymbol> resolver) {
|
||||
myFormat = format;
|
||||
myRange = range;
|
||||
myArgument = argument;
|
||||
myResolver = resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -123,21 +167,24 @@ public final class StringFormatSymbolReferenceProvider implements PsiSymbolRefer
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<? extends Symbol> 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<JavaFormatArgumentSymbol> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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" :
|
||||
"<caret>myFormat: date = %2$s; num = %1$d", 123, date);
|
||||
}
|
||||
}""");
|
||||
PsiLiteralExpression str = getLiteral();
|
||||
Collection<? extends @NotNull PsiSymbolReference> 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 = "<caret>Hello %d %s";
|
||||
System.out.printf(template, 123, s);
|
||||
}
|
||||
}""");
|
||||
PsiLiteralExpression str = getLiteral();
|
||||
Collection<? extends @NotNull PsiSymbolReference> refs = PsiSymbolReferenceService.getService().getReferences(str);
|
||||
assertEquals(2, refs.size());
|
||||
Map<String, String> 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 = "<caret>Hello {1} {1} {0}";
|
||||
System.out.println(MessageFormat.format(template, s, 123));
|
||||
}
|
||||
}""");
|
||||
PsiLiteralExpression str = getLiteral();
|
||||
Collection<? extends @NotNull PsiSymbolReference> refs = PsiSymbolReferenceService.getService().getReferences(str);
|
||||
assertEquals(3, refs.size());
|
||||
Map<String, String> 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<? extends @NotNull PsiSymbolReference> refs = PsiSymbolReferenceService.getService().getReferences(str);
|
||||
assertEquals(2, refs.size());
|
||||
Map<String, String> expected = Map.of("%2$s", "date", "%1$d", "123");
|
||||
return str;
|
||||
}
|
||||
|
||||
private static @NotNull JavaFormatArgumentSymbol resolveRef(@NotNull PsiSymbolReference ref) {
|
||||
Collection<? extends Symbol> 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<? extends PsiSymbolReference> refs,
|
||||
@NotNull PsiLiteralExpression str,
|
||||
@NotNull PsiElement formatString,
|
||||
@NotNull Map<String, String> expected) {
|
||||
for (PsiSymbolReference ref : refs) {
|
||||
assertEquals(str, ref.getElement());
|
||||
String formatSpecifier = ref.getRangeInElement().substring(ref.getElement().getText());
|
||||
Collection<? extends Symbol> 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);
|
||||
}
|
||||
|
||||
@@ -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<MessageFormatErrorType> = setOf(MessageFormatErrorType.INDEX_NEGATIVE,
|
||||
|
||||
Reference in New Issue
Block a user