[java-highlighting] IDEA-378781 Quick fixes for typos in keywords

GitOrigin-RevId: 3984441825712d7f566461eaea10748d5b78f024
This commit is contained in:
Mikhail Pyltsin
2025-09-22 14:16:34 +00:00
committed by intellij-monorepo-bot
parent 9beffc1292
commit eebb6c4bb2
44 changed files with 421 additions and 13 deletions
@@ -543,4 +543,14 @@ public abstract class QuickFixFactory {
*/
public abstract @NotNull List<? extends @NotNull ModCommandAction> createReplaceTypeWithWrongImportFixes(@Nullable PsiJavaCodeReferenceElement reference);
/**
* Creates an action to change an old PsiElement to a similar keyword
* from the provided list of new keywords.
*
* @param old the PsiElement to be replaced; may be null if no element exists.
* @param newKeywords a collection of new keyword strings to replace the old element; must not be null.
* @return a ModCommandAction that represents the change, or null if the action cannot be created.
*/
public abstract @Nullable ModCommandAction createChangeToSimilarKeyword(@Nullable PsiElement old,
@NotNull Collection<@NotNull String> newKeywords);
}
@@ -467,4 +467,7 @@ add.missing.nested.patterns.fix.text=Add missing nested pattern{0, choice, 1#|2#
add.missing.str.processor=Add 'STR.' Processor
record.delegate.to.canonical.constructor.fix.name=Delegate to canonical constructor
lift.throw.out.of.switch.expression.fix.name=Lift 'throw' out of 'switch' expression
lift.throw.out.of.switch.expression.fix.name=Lift 'throw' out of 'switch' expression
change.to.similar.keyword.fix.family.name=Change to the similar keyword
change.to.similar.keyword.fix.name=Change ''{0}'' to ''{1}''
@@ -667,6 +667,7 @@ public final class DefaultJavaErrorFixProvider extends AbstractJavaErrorFixProvi
QualifySuperArgumentFix.registerQuickFixAction(error.context(), sink));
fix(REFERENCE_UNRESOLVED, error -> HighlightFixUtil.createUnresolvedReferenceFix(error.psi()));
fix(REFERENCE_UNRESOLVED, error -> HighlightFixUtil.createVariableTypeFix(error.psi()));
fix(REFERENCE_UNRESOLVED, error -> HighlightFixUtil.createKeywordTypoFix(error.psi()));
fix(REFERENCE_QUALIFIER_PRIMITIVE,
error -> error.psi() instanceof PsiReferenceExpression ref ? myFactory.createRenameWrongRefFix(ref) : null);
fix(CAST_INTERSECTION_NOT_INTERFACE, error -> {
@@ -12,6 +12,7 @@ import com.intellij.java.codeserver.core.JavaPatternExhaustivenessUtil;
import com.intellij.java.codeserver.core.JavaPsiModifierUtil;
import com.intellij.java.codeserver.core.JavaPsiSealedUtil;
import com.intellij.java.codeserver.core.JavaPsiSwitchUtil;
import com.intellij.java.syntax.parser.JavaKeywords;
import com.intellij.lang.jvm.JvmModifier;
import com.intellij.lang.jvm.actions.JvmElementActionFactories;
import com.intellij.lang.jvm.actions.MemberRequestsKt;
@@ -49,6 +50,7 @@ import java.util.function.Consumer;
import static com.intellij.java.codeserver.core.JavaPatternExhaustivenessUtil.checkRecordExhaustiveness;
import static com.intellij.java.codeserver.core.JavaPatternExhaustivenessUtil.findMissedClasses;
import static com.intellij.psi.util.PsiUtil.PRIMITIVE_TYPES;
import static com.intellij.util.ObjectUtils.tryCast;
import static java.util.Objects.requireNonNull;
import static java.util.Objects.requireNonNullElse;
@@ -1141,6 +1143,69 @@ public final class HighlightFixUtil {
QuickFixFactory.getInstance().createSetVariableTypeFix(localVariable, type));
}
static @Nullable CommonIntentionAction createKeywordTypoFix(@Nullable PsiJavaCodeReferenceElement ref) {
if (ref == null) return null;
if (ref.isQualified()) return null;
if (ref.getParent() instanceof PsiTypeElement typeElement) {
//primitive keywords and var
PsiElement parent = typeElement.getParent();
if (parent instanceof PsiClass && typeElement.getNextSibling() instanceof PsiErrorElement) {
return QuickFixFactory.getInstance().createChangeToSimilarKeyword(typeElement, PRIMITIVE_TYPES);
}
if (!(parent instanceof PsiVariable variable &&
!(parent instanceof PsiParameter parameter && !(parameter.getParent() instanceof PsiParameterList)))) {
return null;
}
Set<String> targetKeywords = new HashSet<>(PRIMITIVE_TYPES);
PsiExpression initializer = variable.getInitializer();
if (initializer != null && initializer.getType() != null) {
PsiType initializerType = initializer.getType();
targetKeywords.removeIf(t -> {
PsiPrimitiveType targetType = null;
for (PsiPrimitiveType type : PsiTypes.primitiveTypes()) {
if (t.equals(type.getName())) {
targetType = type;
break;
}
}
if (targetType == null) return true;
return !TypeConversionUtil.isAssignable(targetType, initializerType);
});
}
if (parent instanceof PsiLocalVariable && PsiUtil.isAvailable(JavaFeature.LVTI, parent)) {
targetKeywords.add(JavaKeywords.VAR);
}
return QuickFixFactory.getInstance().createChangeToSimilarKeyword(typeElement, targetKeywords);
}
if (ref.getParent() instanceof PsiExpressionStatement expressionStatement) {
//primitive keywords and var
PsiElement[] children = expressionStatement.getChildren();
if (children.length == 2 && children[0] == ref && children[1] instanceof PsiErrorElement) {
Set<String> targetKeywords = new HashSet<>(PRIMITIVE_TYPES);
if (PsiUtil.isAvailable(JavaFeature.LVTI, expressionStatement)) {
targetKeywords.add(JavaKeywords.VAR);
}
return QuickFixFactory.getInstance().createChangeToSimilarKeyword(ref, targetKeywords);
}
}
if (ref instanceof PsiReferenceExpression expression) {
//true, false, null
Set<String> targetKeywords = new HashSet<>();
PsiType expectedType = ExpectedTypeUtils.findExpectedType(expression, false);
if (expectedType != null) {
if (TypeConversionUtil.isAssignable(expectedType, PsiTypes.booleanType())) {
targetKeywords.add(JavaKeywords.TRUE);
targetKeywords.add(JavaKeywords.FALSE);
}
if (expectedType instanceof PsiClassType) {
targetKeywords.add(JavaKeywords.NULL);
}
return QuickFixFactory.getInstance().createChangeToSimilarKeyword(ref, targetKeywords);
}
}
return null;
}
private static final class ReturnModel {
final PsiReturnStatement myStatement;
final PsiType myType;
@@ -13,6 +13,7 @@ import com.intellij.psi.*
import com.intellij.psi.impl.source.tree.JavaElementType
import com.intellij.psi.util.InheritanceUtil
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.PsiUtil.PRIMITIVE_TYPES
import com.intellij.util.containers.addIfNotNull
@@ -94,7 +95,7 @@ private object TryWithResourcesPositionMatcher: LookupPositionMatcher {
private fun match(lookupElement: LookupElement): Boolean {
val obj = lookupElement.`object`
if (obj is PsiKeyword && obj.text in JavaKeywordCompletion.PRIMITIVE_TYPES) {
if (obj is PsiKeyword && obj.text in PRIMITIVE_TYPES) {
return true
}
val psiClass = obj as? PsiClass ?: return false
@@ -43,6 +43,7 @@ import static com.intellij.codeInsight.completion.JavaCompletionContributor.IN_C
import static com.intellij.openapi.util.Conditions.notInstanceOf;
import static com.intellij.patterns.PsiJavaPatterns.*;
import static com.intellij.psi.SyntaxTraverser.psiApi;
import static com.intellij.psi.util.PsiUtil.PRIMITIVE_TYPES;
public class JavaKeywordCompletion {
public static final ElementPattern<PsiElement> AFTER_DOT = psiElement().afterLeaf(".");
@@ -114,13 +115,6 @@ public class JavaKeywordCompletion {
not(START_SWITCH),
not(JavaMemberNameCompletionContributor.INSIDE_TYPE_PARAMS_PATTERN));
static final Set<String> PRIMITIVE_TYPES = ContainerUtil.newLinkedHashSet(
JavaKeywords.SHORT, JavaKeywords.BOOLEAN,
JavaKeywords.DOUBLE, JavaKeywords.LONG,
JavaKeywords.INT, JavaKeywords.FLOAT,
JavaKeywords.CHAR, JavaKeywords.BYTE
);
static final PsiElementPattern<PsiElement, ?> START_FOR = psiElement().afterLeaf(psiElement().withText("(").afterLeaf("for"));
private static final ElementPattern<PsiElement> CLASS_REFERENCE =
psiElement().withParent(psiReferenceExpression().referencing(psiClass().andNot(psiElement(PsiTypeParameter.class))));
@@ -31,6 +31,7 @@ import java.util.function.Function;
import static com.intellij.patterns.PsiJavaPatterns.elementType;
import static com.intellij.patterns.PsiJavaPatterns.psiElement;
import static com.intellij.patterns.StandardPatterns.or;
import static com.intellij.psi.util.PsiUtil.PRIMITIVE_TYPES;
public class PreferByKindWeigher extends LookupElementWeigher {
public static final Key<Boolean> INTRODUCED_VARIABLE = Key.create("INTRODUCED_VARIABLE");
@@ -356,7 +357,7 @@ public class PreferByKindWeigher extends LookupElementWeigher {
boolean expectsNotNull = ContainerUtil.exists(myExpectedTypes, PreferByKindWeigher::isEnumClass);
return expectsNotNull ? ThreeState.NO : ThreeState.YES;
}
if (JavaKeywordCompletion.PRIMITIVE_TYPES.contains(keyword) || JavaKeywords.VOID.equals(keyword)) {
if (PRIMITIVE_TYPES.contains(keyword) || JavaKeywords.VOID.equals(keyword)) {
boolean inCallArg = psiElement().withParents(PsiReferenceExpression.class, PsiExpressionList.class).accepts(myPosition);
return inCallArg || isInMethodTypeArg(myPosition) ? ThreeState.NO : ThreeState.UNSURE;
}
@@ -0,0 +1,79 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.modcommand.ActionContext;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.Presentation;
import com.intellij.modcommand.PsiUpdateModCommandAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.text.EditDistance;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
public class ChangeToSimilarKeywordFix extends PsiUpdateModCommandAction<PsiElement> {
@NotNull
private final String myKeyword;
private ChangeToSimilarKeywordFix(@NotNull PsiElement old, @NotNull String keyword) {
super(old);
myKeyword = keyword;
}
@Override
protected @Nullable Presentation getPresentation(@NotNull ActionContext context, @NotNull PsiElement element) {
return Presentation.of(QuickFixBundle.message("change.to.similar.keyword.fix.name", element.getText(), myKeyword));
}
@Override
public @NotNull String getFamilyName() {
return QuickFixBundle.message("change.to.similar.keyword.fix.family.name");
}
@Override
protected void invoke(@NotNull ActionContext context,
@NotNull PsiElement element,
@NotNull ModPsiUpdater updater) {
PsiFile file = element.getContainingFile();
if (file == null) return;
Document document = file.getFileDocument();
TextRange textRange = element.getTextRange();
document.replaceString(textRange.getStartOffset(), textRange.getEndOffset(), myKeyword);
}
/**
* Creates a fix if the new keywords are similar enough.
*/
@Nullable
public static ChangeToSimilarKeywordFix createFix(@Nullable PsiElement old, @NotNull Collection<@NotNull String> newKeywords) {
if (old == null || newKeywords.isEmpty()) return null;
String oldText = old.getText();
int minDistance = Integer.MAX_VALUE;
String bestKeyword = null;
int maxLevenshtein;
if (oldText.length() <= 4) {
maxLevenshtein = 1;
}
else {
maxLevenshtein = 2;
}
for (String keyword : newKeywords) {
int levenshtein = EditDistance.optimalAlignment(keyword, oldText, true, maxLevenshtein + 2);
if (levenshtein < minDistance) {
minDistance = levenshtein;
bestKeyword = keyword;
}
}
if (maxLevenshtein < minDistance) {
return null;
}
return new ChangeToSimilarKeywordFix(old, bestKeyword);
}
}
@@ -1175,4 +1175,12 @@ public final class QuickFixFactoryImpl extends QuickFixFactory {
if(reference == null) return List.of();
return ReplaceTypeWithWrongImportFix.createFixes(reference);
}
@Override
public @Nullable ModCommandAction createChangeToSimilarKeyword(@Nullable PsiElement old,
@NotNull Collection<@NotNull String> newKeywords) {
if (old == null) return null;
if (newKeywords.isEmpty()) return null;
return ChangeToSimilarKeywordFix.createFix(old, newKeywords);
}
}
@@ -1538,6 +1538,14 @@ public final class PsiUtil extends PsiUtilCore {
return false;
}
@ApiStatus.Internal
public static final Set<String> PRIMITIVE_TYPES = Collections.unmodifiableSet(ContainerUtil.newLinkedHashSet(
SHORT, BOOLEAN,
DOUBLE, LONG,
INT, FLOAT,
CHAR, BYTE
));
private static final Set<String> KEYWORDS = Collections.unmodifiableSet(
ContainerUtil.newHashSet(ABSTRACT, BOOLEAN, BREAK, BYTE, CASE, CATCH, CHAR, CLASS, CONST, CONTINUE, DEFAULT, DO, DOUBLE, ELSE, EXTENDS,
FALSE, FINAL, FINALLY, FLOAT, FOR, GOTO, IF, IMPLEMENTS, IMPORT, INSTANCEOF, INT, INTERFACE, LONG, NATIVE,
@@ -1298,9 +1298,10 @@ public final class TypeConversionUtil {
PRIMITIVE_TYPES.add(PsiTypes.booleanType().getName());
}
private static final Set<String> PRIMITIVE_WRAPPER_FQNS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
JAVA_LANG_BYTE, JAVA_LANG_CHARACTER, JAVA_LANG_DOUBLE, JAVA_LANG_FLOAT, JAVA_LANG_LONG, JAVA_LANG_INTEGER, JAVA_LANG_SHORT,
JAVA_LANG_BOOLEAN)));
private static final Set<String> PRIMITIVE_WRAPPER_FQNS = Collections.unmodifiableSet(
ContainerUtil.newHashSet(JAVA_LANG_BYTE, JAVA_LANG_CHARACTER, JAVA_LANG_DOUBLE, JAVA_LANG_FLOAT, JAVA_LANG_LONG, JAVA_LANG_INTEGER,
JAVA_LANG_SHORT,
JAVA_LANG_BOOLEAN));
private static final Set<String> PRIMITIVE_WRAPPER_SIMPLE_NAMES =
ContainerUtil.map2Set(PRIMITIVE_WRAPPER_FQNS, StringUtil::getShortName);
@@ -0,0 +1,9 @@
// "Change 'True' to 'true'" "true-preview"
public class Test {
public static void main(String[] args) {
call(true);
}
public static void call(boolean b) {
}
}
@@ -0,0 +1,7 @@
// "Change 'booln' to 'boolean'" "true-preview"
public class Test {
final boolean;
public static void main(String[] args) {
}
}
@@ -0,0 +1,7 @@
// "Change 'booln' to 'boolean'" "true-preview"
public class Test {
boolean a = true;
public static void main(String[] args) {
}
}
@@ -0,0 +1,7 @@
// "Change 'booln' to 'boolean'" "true-preview"
public class Test {
boolean;
public static void main(String[] args) {
}
}
@@ -0,0 +1,7 @@
// "Change 'booln' to 'boolean'" "true-preview"
public class Test {
boolean a;
public static void main(String[] args) {
}
}
@@ -0,0 +1,6 @@
// "Change 'boolen' to 'boolean'" "true-preview"
public class Test {
public static void main(String[] args) {
boolean a;
}
}
@@ -0,0 +1,6 @@
// "Change 'chr' to 'char'" "true-preview"
public class Test {
public static void main(String[] args) {
char
}
}
@@ -0,0 +1,6 @@
// "Change 'Int' to 'int'" "true-preview"
public class Test {
public static void main(String[] args) {
int a = 0;
}
}
@@ -0,0 +1,5 @@
// "Change 'lng' to 'long'" "true-preview"
public class Test {
public static void test(long a) {
}
}
@@ -0,0 +1,9 @@
// "Change 'Null' to 'null'" "true-preview"
public class Test {
public static void main(String[] args) {
call(null);
}
public static void call(Object b) {
}
}
@@ -0,0 +1,6 @@
// "Change 'val' to 'var'" "true-preview"
public class Test {
public static void main(String[] args) {
var a = 0;
}
}
@@ -0,0 +1,6 @@
// "Change 'val' to 'var'" "true-preview"
public class Test {
public static void main(String[] args) {
var
}
}
@@ -0,0 +1,6 @@
// "Change 'val' to 'var'" "true-preview"
public class Test {
public static void main(String[] args) {
var a;
}
}
@@ -0,0 +1,9 @@
// "Change 'True' to 'true'" "true-preview"
public class Test {
public static void main(String[] args) {
call(True<caret>);
}
public static void call(boolean b) {
}
}
@@ -0,0 +1,9 @@
// "Change 'True' to 'true'" "false"
public class Test {
public static void main(String[] args) {
call(True<caret>);
}
public static void call(String b) {
}
}
@@ -0,0 +1,7 @@
// "Change 'booln' to 'boolean'" "true-preview"
public class Test {
final booln<caret>;
public static void main(String[] args) {
}
}
@@ -0,0 +1,7 @@
// "Change 'booln' to 'boolean'" "true-preview"
public class Test {
booln<caret> a = true;
public static void main(String[] args) {
}
}
@@ -0,0 +1,7 @@
// "Change 'booln' to 'boolean'" "true-preview"
public class Test {
booln<caret>;
public static void main(String[] args) {
}
}
@@ -0,0 +1,7 @@
// "Change 'booln' to 'boolean'" "true-preview"
public class Test {
booln<caret> a;
public static void main(String[] args) {
}
}
@@ -0,0 +1,6 @@
// "Change 'boolen' to 'boolean'" "true-preview"
public class Test {
public static void main(String[] args) {
boolen<caret> a;
}
}
@@ -0,0 +1,8 @@
// "Change 'val' to 'var'" "false"
public class Test {
public static void test() {
try {
}catch (val<caret> e)
}
}
@@ -0,0 +1,6 @@
// "Change 'chr' to 'char'" "true-preview"
public class Test {
public static void main(String[] args) {
chr<caret>
}
}
@@ -0,0 +1,6 @@
// "Change 'Int' to 'int'" "true-preview"
public class Test {
public static void main(String[] args) {
Int<caret> a = 0;
}
}
@@ -0,0 +1,6 @@
// "Change 'Int' to 'int'" "false"
public class Test {
public static void main(String[] args) {
Int<caret> a = 1.0;
}
}
@@ -0,0 +1,5 @@
// "Change 'lng' to 'long'" "true-preview"
public class Test {
public static void test(lng<caret> a) {
}
}
@@ -0,0 +1,9 @@
// "Change 'Null' to 'null'" "true-preview"
public class Test {
public static void main(String[] args) {
call(Null<caret>);
}
public static void call(Object b) {
}
}
@@ -0,0 +1,9 @@
// "Change 'Null' to 'null'" "false"
public class Test {
public static void main(String[] args) {
call(Null<caret>);
}
public static void call(boolean b) {
}
}
@@ -0,0 +1,7 @@
// "Change 'val' to 'var'" "false"
public class Test {
val<caret> a = 1;
public static void main(String[] args) {
}
}
@@ -0,0 +1,5 @@
// "Change 'val' to 'var'" "false"
public class Test {
public static void test(val<caret> a) {
}
}
@@ -0,0 +1,6 @@
// "Change 'val' to 'var'" "true-preview"
public class Test {
public static void main(String[] args) {
val<caret> a = 0;
}
}
@@ -0,0 +1,6 @@
// "Change 'val' to 'var'" "true-preview"
public class Test {
public static void main(String[] args) {
val<caret>
}
}
@@ -0,0 +1,6 @@
// "Change 'val' to 'var'" "true-preview"
public class Test {
public static void main(String[] args) {
val<caret> a;
}
}
@@ -0,0 +1,19 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.testFramework.IdeaTestUtil;
public class ChangeToSimilarKeywordFixTest extends LightQuickFixParameterizedTestCase {
@Override
protected Sdk getProjectJDK() {
return IdeaTestUtil.getMockJdk21();
}
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/changeToSimilarKeywordFix";
}
}