diff --git a/java/java-impl/src/META-INF/JavaPlugin.xml b/java/java-impl/src/META-INF/JavaPlugin.xml index 749638d127d8..af4d1aa3e758 100644 --- a/java/java-impl/src/META-INF/JavaPlugin.xml +++ b/java/java-impl/src/META-INF/JavaPlugin.xml @@ -306,6 +306,8 @@ + + @@ -1007,6 +1009,7 @@ + @@ -1267,6 +1270,7 @@ + @@ -2515,8 +2519,8 @@ - + @@ -2550,6 +2554,8 @@ + diff --git a/java/java-impl/src/com/intellij/codeEditor/JavaBinaryFileSourceProvider.java b/java/java-impl/src/com/intellij/codeEditor/JavaBinaryFileSourceProvider.java new file mode 100644 index 000000000000..40e4b882c2ae --- /dev/null +++ b/java/java-impl/src/com/intellij/codeEditor/JavaBinaryFileSourceProvider.java @@ -0,0 +1,19 @@ +// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeEditor; + +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.platform.indexing.BinaryFileSourceProvider; +import com.intellij.psi.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class JavaBinaryFileSourceProvider implements BinaryFileSourceProvider { + + @Override + public @Nullable PsiFile findSourceFile(@NotNull PsiBinaryFile file) { + VirtualFile virtualFile = JavaEditorFileSwapper.findSourceFile(file.getProject(), file.getVirtualFile()); + PsiManager manager = file.getManager(); + if (virtualFile == null) return null; + return manager.findFile(virtualFile); + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/ConstantPoolParser.java b/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/ConstantPoolParser.java new file mode 100644 index 000000000000..a7745fa4e9fc --- /dev/null +++ b/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/ConstantPoolParser.java @@ -0,0 +1,110 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.psi.impl.cache.impl.idCache; + +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.io.DataInput; +import java.io.IOException; + +@ApiStatus.Internal +public final class ConstantPoolParser { + private final @NotNull DataInput myInput; + private final @NotNull Callback myCallback; + + public ConstantPoolParser(@NotNull DataInput input, @NotNull Callback callback) { + myInput = input; + myCallback = callback; + } + + public void parse() throws IOException, ClassFormatException { + int magic = myInput.readInt(); + if(magic != 0xCAFEBABE) { + throw new ClassFormatException("Invalid class file: magic incorrect"); + } + + // version + myInput.readShort(); + myInput.readShort(); + + int constantPoolCount = myInput.readShort(); + + for(int i = 0; i < constantPoolCount - 1;) { + i += readConstant(); + } + } + + /** + * @return number of slots occupied in constant pool + */ + @SuppressWarnings("DuplicateBranchesInSwitch") + private int readConstant() throws IOException, ClassFormatException { + int tag = myInput.readByte(); + switch (tag) { + case 1 -> { + String utf8String = myInput.readUTF(); + myCallback.onUtf8(utf8String); // Utf8 + } + case 3 -> // Integer + myInput.skipBytes(4); // Integer value + case 4 -> // Float + myInput.skipBytes(4); // Float value + case 5 -> {// Long + myInput.skipBytes(8); // Long value + return 2; + } + case 6 -> { // Double + myInput.skipBytes(8); // Double value + return 2; + } + case 7 -> // Class + myInput.skipBytes(2); // Class index + case 8 -> // String + myInput.skipBytes(2); // String index + case 9 -> { // Fieldref + myInput.skipBytes(4); // Class index + NameAndType index + } + case 10 -> { // Methodref + myInput.skipBytes(4); // Class index + NameAndType index + } + case 11 -> { // InterfaceMethodref + myInput.skipBytes(4); // Class index + NameAndType index + } + case 12 -> { // NameAndType + myInput.skipBytes(4); // Name index + Type index + } + case 15 -> { // MethodHandle + myInput.skipBytes(3); // Reference kind + Reference index + } + case 16 -> // MethodType + myInput.skipBytes(2); // Descriptor index + case 17 -> { // Dynamic + myInput.skipBytes(4); // Bootstrap method index + NameAndType index + } + case 18 -> { // InvokeDynamic + myInput.skipBytes(4); // Bootstrap method index + NameAndType index + } + case 19 -> // Module + myInput.skipBytes(2); // Name index + case 20 -> // Package + myInput.skipBytes(2); // Name index + default -> throw new ClassFormatException("Invalid constant pool entry tag: " + tag); + } + return 1; + } + + public interface Callback { + void onUtf8(String str); + } + + public static class ClassFormatException extends Exception { + ClassFormatException(String message) { + super(message); + } + } + + public static void main(String[] args) { + System.out.println("Is java id \uD83D\uDC69\u200D: " + StringUtil.isJavaIdentifier("\uD83D\uDC69\u200D")); + } +} diff --git a/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/JavaIdIndexRegistryValueListener.kt b/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/JavaIdIndexRegistryValueListener.kt new file mode 100644 index 000000000000..6069c4861efb --- /dev/null +++ b/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/JavaIdIndexRegistryValueListener.kt @@ -0,0 +1,16 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.psi.impl.cache.impl.idCache + +import com.intellij.openapi.util.registry.RegistryValue +import com.intellij.openapi.util.registry.RegistryValueListener +import com.intellij.psi.impl.cache.impl.id.IdIndex +import com.intellij.util.indexing.FileBasedIndex + +class JavaIdIndexRegistryValueListener : RegistryValueListener { + override fun afterValueChanged(value: RegistryValue) { + if (JavaIdIndexer.ENABLED_REGISTRY_KEY == value.key) { + JavaIdIndexer.isEnabled = value.asBoolean() + FileBasedIndex.getInstance().requestRebuild(IdIndex.NAME) + } + } +} \ No newline at end of file diff --git a/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/JavaIdIndexer.java b/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/JavaIdIndexer.java index 31b2495acbba..ac932f395755 100644 --- a/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/JavaIdIndexer.java +++ b/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/JavaIdIndexer.java @@ -1,17 +1,89 @@ -// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2019 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 com.intellij.psi.impl.cache.impl.idCache; +import com.intellij.ide.highlighter.JavaClassFileType; import com.intellij.lang.java.JavaParserDefinition; import com.intellij.lexer.Lexer; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.java.LanguageLevel; import com.intellij.psi.impl.cache.impl.OccurrenceConsumer; -import com.intellij.psi.impl.cache.impl.id.LexerBasedIdIndexer; +import com.intellij.psi.impl.cache.impl.id.IdDataConsumer; +import com.intellij.psi.impl.cache.impl.id.IdIndexEntry; +import com.intellij.psi.impl.cache.impl.id.IdIndexer; +import com.intellij.psi.impl.source.JavaFileElementType; +import com.intellij.psi.search.UsageSearchContext; +import com.intellij.util.indexing.FileContent; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.util.Map; + +import static com.intellij.psi.impl.cache.impl.BaseFilterLexerUtil.scanContentWithCheckCanceled; + +public final class JavaIdIndexer implements IdIndexer { + private final static Logger LOG = Logger.getInstance(JavaIdIndexer.class); + static final String ENABLED_REGISTRY_KEY = "index.ids.from.java.sources.in.jar"; + static volatile boolean isEnabled = Registry.is("index.ids.from.java.sources.in.jar", true); -public final class JavaIdIndexer extends LexerBasedIdIndexer { @Override - public @NotNull Lexer createLexer(final @NotNull OccurrenceConsumer consumer) { - return createIndexingLexer(consumer); + public @NotNull Map map(@NotNull FileContent inputData) { + + VirtualFile file = inputData.getFile(); + String extension = file.getExtension(); + if (extension != null && extension.equals(JavaClassFileType.DEFAULT_EXTENSION) && isEnabled) { + Map idEntries = calculateIdEntriesParsingConstantPool(inputData); + if (idEntries != null) return idEntries; + return Map.of(); + } + // we are skipping indexing of sources in libraries (we are going to index only the compiled library classes) + if (!isEnabled && !JavaFileElementType.isInSourceContent(file)) { + return Map.of(); + } + + IdDataConsumer consumer = new IdDataConsumer(); + scanContentWithCheckCanceled(inputData, createIndexingLexer(new OccurrenceConsumer(consumer, false))); + return consumer.getResult(); + } + + @Nullable + private static Map calculateIdEntriesParsingConstantPool(@NotNull FileContent inputData) { + IdDataConsumer consumer = new IdDataConsumer(); + // optimised to avoid creation of list for every UTF8 entry + IntList startInclList = new IntArrayList(); + IntList endExclList = new IntArrayList(); + ConstantPoolParser parser = new ConstantPoolParser(new DataInputStream(new ByteArrayInputStream(inputData.getContent())), str -> { + ProgressManager.checkCanceled(); + if (str.isEmpty()) return; + // it is likely a method signature, we don't need to handle it because all its parts will it is possible to find in the other entries. + if (str.charAt(0) == '(') return; + + if (JvmIdentifierUtil.collectJvmIdentifiers(str, (sequence, startIncl, endExcl) -> { + startInclList.add(startIncl); + endExclList.add(startIncl); + })) { + for (int i = 0; i < startInclList.size(); i++) { + consumer.addOccurrence(str, startInclList.getInt(i), endExclList.getInt(i), UsageSearchContext.IN_CODE); + } + } + startInclList.clear(); + endExclList.clear(); + }); + try { + parser.parse(); + } + catch (IOException | ConstantPoolParser.ClassFormatException e) { + LOG.warn("Exception while handling file " + inputData.getFileName(), e); + return null; + } + return consumer.getResult(); } public static Lexer createIndexingLexer(OccurrenceConsumer consumer) { diff --git a/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/JvmIdentifierUtil.java b/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/JvmIdentifierUtil.java new file mode 100644 index 000000000000..3f59b902683a --- /dev/null +++ b/java/java-impl/src/com/intellij/psi/impl/cache/impl/idCache/JvmIdentifierUtil.java @@ -0,0 +1,113 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.psi.impl.cache.impl.idCache; + +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +public final class JvmIdentifierUtil { + private JvmIdentifierUtil() { } + + public interface IdConsumer { + /** + * Handles valid JVM identifier extracted from the string + * Warning: Implementation must be ready for rollback - several valid identifiers may be fed to {@code accept} before checking that there + * is an invalid one present in this string and all previous ones should be rolled back. + * @param sequence original sequence, from which the identifiers are extracted. Args correspond to offsets in the sequence. + */ + void accept(@NotNull CharSequence sequence, int startIncl, int endExcl); + } + + /** + * Collects JVM identifiers from a given string and passes them to the provided IdConsumer. + * + * @param str The string to collect JVM identifiers from. + * @param partsConsumer The IdConsumer that will receive the collected JVM identifiers. + * @return true if JVM identifiers are collected successfully, false otherwise. + */ + @Contract(mutates = "param2") + public static boolean collectJvmIdentifiers(@NotNull String str, @NotNull IdConsumer partsConsumer) { + if (str.isEmpty()) return false; + + if (str.charAt(str.length() - 1) == ';' || str.charAt(0) == '(') { + // probably field descriptor, e.g. [[Ljava/lang/String; or [B or S + // also it may be Signature, e.g. + // Ljava/util/Map<[Ljava/lang/String;Ljava/util/List;>;>;>; + // It doesn't make sense to take the strings out of it recursively, so instead parts between 'L' and (';' or '<') are searched + int current = 0; + int start; + outer: + while (current < str.length()) { + char firstChar = str.charAt(current); + if (firstChar == 'L') { + current++; + start = current; + while (current < str.length()) { + char ch = str.charAt(current); + current++; + if (ch == ';' || ch == '<') { + if (!collectSlashSeparated(str, start, current - 1, partsConsumer)) { + return false; + } + continue outer; + } + } + } + current++; + } + if (current == str.length()) { + return true; + } + } + + return collectSlashSeparated(str, 0, str.length(), partsConsumer); + } + + private static int indexOfSlash(String str, int startIndexIncl, int endIndexExcl) { + int current = startIndexIncl; + while (current < endIndexExcl) { + char ch = str.charAt(current); + if (ch == '/') { + return current; + } + + current++; + } + return -1; + } + + private static boolean collectSlashSeparated(@NotNull String str, int startIncl, int endExcl, @NotNull IdConsumer partsConsumer) { + int partStartIncl = startIncl; + while (partStartIncl < endExcl) { + int indexOfSlash = indexOfSlash(str, partStartIncl, endExcl); + if (indexOfSlash == -1) { + break; + } + if (partStartIncl == indexOfSlash) { + return false; + } + if (!StringUtil.isJavaIdentifier(str, partStartIncl, indexOfSlash)) { + return false; + } + partsConsumer.accept(str, partStartIncl, indexOfSlash); + partStartIncl = indexOfSlash + 1; + if (partStartIncl == endExcl) { // '/' is at the end + return false; + } + } + // handling rest + if (partStartIncl == 0) { + if (!StringUtil.isJavaIdentifier(str, 0, str.length())) { + return false; + } + partsConsumer.accept(str, partStartIncl, endExcl); + } else { + if (!StringUtil.isJavaIdentifier(str, partStartIncl, endExcl)) { + return false; + } + partsConsumer.accept(str, partStartIncl, endExcl); + } + return true; + } + +} diff --git a/java/java-tests/testData/psi/constantPool/AnnotationDeclaration.class b/java/java-tests/testData/psi/constantPool/AnnotationDeclaration.class new file mode 100644 index 000000000000..88a78a28a0d1 Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/AnnotationDeclaration.class differ diff --git a/java/java-tests/testData/psi/constantPool/Dollar$InClass.class b/java/java-tests/testData/psi/constantPool/Dollar$InClass.class new file mode 100644 index 000000000000..609d6b28a07a Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/Dollar$InClass.class differ diff --git a/java/java-tests/testData/psi/constantPool/DoubleAndLongFields.class b/java/java-tests/testData/psi/constantPool/DoubleAndLongFields.class new file mode 100644 index 000000000000..7207b3b1ef77 Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/DoubleAndLongFields.class differ diff --git a/java/java-tests/testData/psi/constantPool/MethodWithExtendsWildcard.class b/java/java-tests/testData/psi/constantPool/MethodWithExtendsWildcard.class new file mode 100644 index 000000000000..367de0de81b6 Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/MethodWithExtendsWildcard.class differ diff --git a/java/java-tests/testData/psi/constantPool/MethodWithSuperWildcard.class b/java/java-tests/testData/psi/constantPool/MethodWithSuperWildcard.class new file mode 100644 index 000000000000..9685606b53cd Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/MethodWithSuperWildcard.class differ diff --git a/java/java-tests/testData/psi/constantPool/MethodWithTypeParam.class b/java/java-tests/testData/psi/constantPool/MethodWithTypeParam.class new file mode 100644 index 000000000000..e6774c6c073a Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/MethodWithTypeParam.class differ diff --git a/java/java-tests/testData/psi/constantPool/MethodWithTypeParamExtends.class b/java/java-tests/testData/psi/constantPool/MethodWithTypeParamExtends.class new file mode 100644 index 000000000000..9b2427bce3dc Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/MethodWithTypeParamExtends.class differ diff --git a/java/java-tests/testData/psi/constantPool/MethodWithWildcard.class b/java/java-tests/testData/psi/constantPool/MethodWithWildcard.class new file mode 100644 index 000000000000..fc08483b8bdc Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/MethodWithWildcard.class differ diff --git a/java/java-tests/testData/psi/constantPool/MyEnum.class b/java/java-tests/testData/psi/constantPool/MyEnum.class new file mode 100644 index 000000000000..1b1148fe6286 Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/MyEnum.class differ diff --git a/java/java-tests/testData/psi/constantPool/RuntimeAnnotation.class b/java/java-tests/testData/psi/constantPool/RuntimeAnnotation.class new file mode 100644 index 000000000000..623185632783 Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/RuntimeAnnotation.class differ diff --git a/java/java-tests/testData/psi/constantPool/RuntimeAnnotationWithArgs.class b/java/java-tests/testData/psi/constantPool/RuntimeAnnotationWithArgs.class new file mode 100644 index 000000000000..93ba0fdabc42 Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/RuntimeAnnotationWithArgs.class differ diff --git a/java/java-tests/testData/psi/constantPool/SeveralBounds.class b/java/java-tests/testData/psi/constantPool/SeveralBounds.class new file mode 100644 index 000000000000..6d238e67b187 Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/SeveralBounds.class differ diff --git a/java/java-tests/testData/psi/constantPool/SeveralBoundsInMethod.class b/java/java-tests/testData/psi/constantPool/SeveralBoundsInMethod.class new file mode 100644 index 000000000000..76565a0ed199 Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/SeveralBoundsInMethod.class differ diff --git a/java/java-tests/testData/psi/constantPool/Simple.class b/java/java-tests/testData/psi/constantPool/Simple.class new file mode 100644 index 000000000000..29ab0a22c304 Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/Simple.class differ diff --git a/java/java-tests/testData/psi/constantPool/TryCatch.class b/java/java-tests/testData/psi/constantPool/TryCatch.class new file mode 100644 index 000000000000..1ec3a1d7c398 Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/TryCatch.class differ diff --git a/java/java-tests/testData/psi/constantPool/TypeParameterInClass.class b/java/java-tests/testData/psi/constantPool/TypeParameterInClass.class new file mode 100644 index 000000000000..c819ad8fc034 Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/TypeParameterInClass.class differ diff --git a/java/java-tests/testData/psi/constantPool/WithParameter.class b/java/java-tests/testData/psi/constantPool/WithParameter.class new file mode 100644 index 000000000000..8a21a8edcaab Binary files /dev/null and b/java/java-tests/testData/psi/constantPool/WithParameter.class differ diff --git a/java/java-tests/testSrc/com/intellij/psi/impl/ConstantPoolParserTest.kt b/java/java-tests/testSrc/com/intellij/psi/impl/ConstantPoolParserTest.kt new file mode 100644 index 000000000000..6ecbbfb1dff3 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/psi/impl/ConstantPoolParserTest.kt @@ -0,0 +1,545 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.psi.impl + +import com.intellij.psi.impl.cache.impl.idCache.ConstantPoolParser +import com.intellij.psi.impl.cache.impl.idCache.JvmIdentifierUtil.collectJvmIdentifiers +import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase5 +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import java.io.ByteArrayInputStream +import java.io.DataInputStream +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.readBytes + +class ConstantPoolParserTest : LightJavaCodeInsightFixtureTestCase5() { + private val constantPoolPath: Path by lazy { Paths.get(fixture.testDataPath).resolve("psi").resolve("constantPool") } + + /** + * Source: + * ``` + * public class Simple { + * String field = "my field"; + * + * void method() { + * + * } + * } + * + * ``` + */ + @Test fun testSimple() = doTest(""" + Code + LineNumberTable + LocalVariableTable + Object + Simple + SourceFile + String + field + java + lang + method + this + """.trimIndent()) + + /** + * Source: + * ``` + * public class WithParameter { + * public static void foo(String[] s) {} + * public static void bar(Map>>> s) {} + * } + * ``` + */ + @Test fun testWithParameter() = doTest(""" + Code + Comparator + LineNumberTable + List + LocalVariableTable + LocalVariableTypeTable + Map + Object + Signature + SourceFile + String + WithParameter + bar + example + foo + java + lang + org + s + this + util + """.trimIndent()) + + /** + * Source: + * ``` + * @RuntimeAnnotation.RuntimeAnn + * public class RuntimeAnnotation { + * @Retention(RetentionPolicy.RUNTIME) + * @Target(ElementType.TYPE) + * @interface RuntimeAnn { + * + * } + * } + * + * ``` + * + */ + @Test fun testRuntimeAnnotation() = doTest(""" + Code + InnerClasses + LineNumberTable + LocalVariableTable + Object + RuntimeAnn + RuntimeAnnotation + RuntimeAnnotation${'$'}RuntimeAnn + RuntimeVisibleAnnotations + SourceFile + java + lang + this + """.trimIndent()) + + /** + * Source: + * ``` + * @RuntimeAnnotationWithArgs.RuntimeAnn(justEnum = RuntimeAnnotationWithArgs.MyEnum.Variant2) + * public class RuntimeAnnotationWithArgs { + * @Retention(RetentionPolicy.RUNTIME) + * @Target(ElementType.TYPE) + * @interface RuntimeAnn { + * MyEnum enumWithDefault() default MyEnum.Variant1; + * MyEnum justEnum(); + * } + * + * enum MyEnum { + * Variant1, + * Variant2, + * } + * } + * ``` + */ + @Test fun testRuntimeAnnotationWithArgs() = doTest(""" + Code + InnerClasses + LineNumberTable + LocalVariableTable + MyEnum + Object + RuntimeAnn + RuntimeAnnotationWithArgs + RuntimeAnnotationWithArgs${'$'}MyEnum + RuntimeAnnotationWithArgs${'$'}RuntimeAnn + RuntimeVisibleAnnotations + SourceFile + Variant2 + java + justEnum + lang + this + """.trimIndent()) + + /** + * Source: + * ``` + * package pack; + * + * public @interface AnnotationDeclaration { + * String stringValue() default "my default value"; + * MyEnum enumValue(); + * + * enum MyEnum { + * Variant1, + * Variant2, + * } + * } + * ``` + */ + @Test fun testAnnotationDeclaration() = doTest(""" + Annotation + AnnotationDeclaration + AnnotationDeclaration${'$'}MyEnum + AnnotationDefault + InnerClasses + MyEnum + Object + SourceFile + String + annotation + enumValue + java + lang + pack + stringValue + """.trimIndent()) + + /** + * Source: + * ``` + *public enum MyEnum { + * Variant1, + * Variant2, + * } + * ``` + */ + @Test fun testMyEnum() = doTest(""" + ${'$'}VALUES + Class + Code + Enum + LineNumberTable + LocalVariableTable + MyEnum + Object + Signature + SourceFile + String + Variant1 + Variant2 + clone + java + lang + name + this + valueOf + values + """.trimIndent()) + + /** + * Source: + * ``` + * public class DoubleAndLongFields { + * double d = 10.0; + * long l = 4L; + * } + * ``` + */ + @Test fun testDoubleAndLongFields() = doTest(""" + Code + D + DoubleAndLongFields + J + LineNumberTable + LocalVariableTable + Object + SourceFile + d + java + l + lang + this + """.trimIndent()) + /** + * Source: + * ``` + * public class TryCatch { + * void foo() { + * try { + * bar(); + * } catch (RuntimeException e) { + * + * } + * } + * + * native void bar(); + * } + * + * ``` + */ + @Test fun testTryCatch() = doTest(""" + Code + LineNumberTable + LocalVariableTable + Object + RuntimeException + SourceFile + StackMapTable + TryCatch + bar + foo + java + lang + this + """.trimIndent()) + + /** + * Source: + * ``` + * import java.util.List; + * + * public class MethodWithExtendsWildcard { + * public void id(List param) {} + * } + * + * ``` + */ + @Test fun testMethodWithExtendsWildcard() { + doTest(""" + CharSequence + Code + LineNumberTable + List + LocalVariableTable + LocalVariableTypeTable + MethodWithExtendsWildcard + Object + Signature + SourceFile + id + java + lang + param + this + util + """.trimIndent()) + } + + /** + * Source: + * ``` + * import java.util.List; + * + * public class MethodWithSuperWildcard { + * public void id(List param) {} + * } + * + * ``` + */ + @Test fun testMethodWithSuperWildcard() { + doTest(""" + CharSequence + Code + LineNumberTable + List + LocalVariableTable + LocalVariableTypeTable + MethodWithSuperWildcard + Object + Signature + SourceFile + id + java + lang + param + this + util + """.trimIndent()) + } + + /** + * Source: + * ``` + * public class MethodWithTypeParam { + * public void id(TYPE_PARAM param) {} + * } + * ``` + */ + @Test fun testMethodWithTypeParam() { + doTest(""" + Code + LineNumberTable + LocalVariableTable + LocalVariableTypeTable + MethodWithTypeParam + Object + Signature + SourceFile + id + java + lang + param + this + """.trimIndent()) + } + + /** + * Source: + * ``` + * public class MethodWithTypeParamExtends { + * public void id(TYPE_PARAM param) {} + * } + * ``` + */ + @Test fun testMethodWithTypeParamExtends() { + doTest(""" + CharSequence + Code + LineNumberTable + LocalVariableTable + LocalVariableTypeTable + MethodWithTypeParamExtends + Object + Signature + SourceFile + id + java + lang + param + this + """.trimIndent()) + } + + /** + * Source: + * ``` + * import java.util.List; + * + * public class MethodWithWildcard { + * public void id(List param) {} + * } + * ``` + */ + @Test fun testMethodWithWildcard() { + doTest(""" + Code + LineNumberTable + List + LocalVariableTable + LocalVariableTypeTable + MethodWithWildcard + Object + Signature + SourceFile + id + java + lang + param + this + util + """.trimIndent()) + } + + /** + * Source: + * ``` + * public class TypeParameterInClass {} + * ``` + */ + @Test fun testTypeParameterInClass() { + doTest(""" + CharSequence + Code + LineNumberTable + LocalVariableTable + LocalVariableTypeTable + Object + Signature + SourceFile + TypeParameterInClass + java + lang + this + """.trimIndent()) + } + + /** + * Source: + * ``` + * import java.util.List; + * + * public class SeveralBounds & List> { + * } + * ``` + */ + @Test fun testSeveralBounds() { + doTest(""" + Code + Comparable + Integer + LineNumberTable + List + LocalVariableTable + LocalVariableTypeTable + Object + SeveralBounds + Signature + SourceFile + String + java + lang + this + util + """.trimIndent()) + } + + /** + * Source: + * ``` + * import java.util.List; + * + * public class SeveralBoundsInMethod { + * >void foo() {} + * } + * ``` + */ + @Test fun testSeveralBoundsInMethod() { + doTest(""" + Code + LineNumberTable + LocalVariableTable + Object + SeveralBoundsInMethod + Signature + SourceFile + foo + java + lang + this + """.trimIndent()) + } + + /** + * Source: + * ``` + * import java.util.List; + * + * public class Dollar$InClass { + * void dollarIn$Method() {} + * int $inField; + * } + * + * ``` + */ + @Test fun `testDollar$InClass`() { + doTest(""" + ${'$'}inField + Code + Dollar${'$'}InClass + I + LineNumberTable + LocalVariableTable + Object + SourceFile + dollarIn${'$'}Method + java + lang + this + """.trimIndent()) + } + + private fun doTest(text: String) { + val filePath = constantPoolPath.resolve(getTestName(false) + ".class") + val bytes = filePath.readBytes() + val ids = extractIdsFromClassFile(bytes) + Assertions.assertEquals(text.trim(), ids.joinToString("\n").trim()) + } +} + +internal fun extractIdsFromClassFile(bytes: ByteArray): List { + val parts = ArrayList() + ConstantPoolParser(DataInputStream(ByteArrayInputStream(bytes))) { + val tmp = ArrayList() + if (collectJvmIdentifiers(it) { sequence: CharSequence, start: Int, end: Int -> tmp.add(sequence.subSequence(start, end)) }) { + parts.addAll(tmp) + } + }.parse() + val ids = parts.map { it.toString() }.toSet().sorted() + return ids +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/psi/impl/JvmIdentifierUtilTest.java b/java/java-tests/testSrc/com/intellij/psi/impl/JvmIdentifierUtilTest.java new file mode 100644 index 000000000000..2be8590a1e91 --- /dev/null +++ b/java/java-tests/testSrc/com/intellij/psi/impl/JvmIdentifierUtilTest.java @@ -0,0 +1,45 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.psi.impl; + +import com.intellij.psi.impl.cache.impl.idCache.JvmIdentifierUtil; +import com.intellij.util.containers.ContainerUtil; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +public class JvmIdentifierUtilTest { + @Test + void testIdentifierExtraction() { + Assertions.assertEquals(List.of("foo"), getJvmIdParts("foo")); + Assertions.assertEquals(List.of("foo", "bar"), getJvmIdParts("foo/bar")); + Assertions.assertEquals(List.of("a", "bb", "ccc"), getJvmIdParts("a/bb/ccc")); + Assertions.assertEquals(List.of("aaa", "bb", "c"), getJvmIdParts("aaa/bb/c")); + Assertions.assertEquals(List.of("java", "lang", "String"), getJvmIdParts("[[Ljava/lang/String;")); + Assertions.assertEquals(List.of("java", "lang", "Comparable", "java", "lang", "String", "java", "util", "List", "java", "lang", "Integer", "java", "lang", "Object"), getJvmIdParts(";:Ljava/util/List;>Ljava/lang/Object;")); + Assertions.assertEquals(List.of( + "java", "util", "Map", "java", "lang", "String", "java", "util", "List", "java", "util", "List", "java", "util", "Comparator", "java", "lang", "String" + ), getJvmIdParts("Ljava/util/Map<[Ljava/lang/String;Ljava/util/List;>;>;>;")); + Assertions.assertEquals(List.of("foo$bar"), getJvmIdParts("foo$bar")); + Assertions.assertNull(getJvmIdParts("1")); + Assertions.assertNull(getJvmIdParts("1/bar")); + Assertions.assertNull(getJvmIdParts("/")); + Assertions.assertNull(getJvmIdParts("with/spa ce")); + Assertions.assertNull(getJvmIdParts("asd*")); + Assertions.assertNull(getJvmIdParts("My sentence with spaces")); + Assertions.assertNull(getJvmIdParts("")); + Assertions.assertNull(getJvmIdParts("foo/")); + Assertions.assertNull(getJvmIdParts("/foo")); + } + + @Nullable + List getJvmIdParts(String str) { + ArrayList parts = new ArrayList<>(); + if (JvmIdentifierUtil.collectJvmIdentifiers(str, (sequence1, start, end) -> parts.add(sequence1.subSequence(start, end)))) { + return ContainerUtil.map(parts, sequence -> sequence.toString()); + } + return null; + } +} diff --git a/platform/indexing-api/api-dump-unreviewed.txt b/platform/indexing-api/api-dump-unreviewed.txt index 67f7db5c0073..8c3f9563600f 100644 --- a/platform/indexing-api/api-dump-unreviewed.txt +++ b/platform/indexing-api/api-dump-unreviewed.txt @@ -240,6 +240,9 @@ c:com.intellij.lang.findUsages.LanguageFindUsages - a:getElement():com.intellij.psi.PsiElement - a:getOffsetInElement():I - s:of(com.intellij.psi.PsiElement,I):com.intellij.model.search.TextOccurrence +com.intellij.platform.indexing.BinaryFileSourceProvider +- sf:EP:com.intellij.openapi.extensions.ExtensionPointName +- a:findSourceFile(com.intellij.psi.PsiBinaryFile):com.intellij.psi.PsiFile com.intellij.psi.impl.cache.CacheManager - a:getFilesWithWord(java.lang.String,S,com.intellij.psi.search.GlobalSearchScope,Z):com.intellij.psi.PsiFile[] - s:getInstance(com.intellij.openapi.project.Project):com.intellij.psi.impl.cache.CacheManager diff --git a/platform/indexing-api/resources/META-INF/Indexing.xml b/platform/indexing-api/resources/META-INF/Indexing.xml index 3f50a794316a..d2c56e1f3e5a 100644 --- a/platform/indexing-api/resources/META-INF/Indexing.xml +++ b/platform/indexing-api/resources/META-INF/Indexing.xml @@ -22,6 +22,7 @@ + diff --git a/platform/indexing-api/src/com/intellij/platform/indexing/BinaryFileSourceProvider.java b/platform/indexing-api/src/com/intellij/platform/indexing/BinaryFileSourceProvider.java new file mode 100644 index 000000000000..cfbff6bba697 --- /dev/null +++ b/platform/indexing-api/src/com/intellij/platform/indexing/BinaryFileSourceProvider.java @@ -0,0 +1,27 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.indexing; + +import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.psi.PsiBinaryFile; +import com.intellij.psi.PsiFile; +import com.intellij.util.concurrency.annotations.RequiresReadLock; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * In the situation when the identifiers are provided by the binary file (implemented {@link com.intellij.psi.impl.cache.impl.id.IdIndexer} + * for binary file) which corresponds to some source file, this extension is used to find the original source file during the Find Usages. + */ +public interface BinaryFileSourceProvider { + ExtensionPointName EP = new ExtensionPointName<>("com.intellij.binaryFileSourceProvider"); + + /** + * Finds the original source file corresponding to the given binary file. + * + * @param file the binary file for which to find the source file + * @return the original source file, or {@code null} if not found + */ + @Nullable + @RequiresReadLock + PsiFile findSourceFile(@NotNull PsiBinaryFile file); +} diff --git a/platform/indexing-impl/src/com/intellij/psi/impl/cache/impl/BaseFilterLexerUtil.java b/platform/indexing-impl/src/com/intellij/psi/impl/cache/impl/BaseFilterLexerUtil.java index f03415d7e791..2186ee356e29 100644 --- a/platform/indexing-impl/src/com/intellij/psi/impl/cache/impl/BaseFilterLexerUtil.java +++ b/platform/indexing-impl/src/com/intellij/psi/impl/cache/impl/BaseFilterLexerUtil.java @@ -12,6 +12,7 @@ import com.intellij.psi.impl.cache.impl.todo.TodoIndexers; import com.intellij.psi.search.IndexPattern; import com.intellij.testFramework.LightVirtualFile; import com.intellij.util.indexing.FileContent; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.Collections; @@ -48,7 +49,8 @@ public final class BaseFilterLexerUtil { return todoMap != null ? todoMap : Collections.emptyMap(); } - private static void scanContentWithCheckCanceled(@NotNull FileContent content, @NotNull Lexer filterLexer) { + @ApiStatus.Internal + public static void scanContentWithCheckCanceled(@NotNull FileContent content, @NotNull Lexer filterLexer) { filterLexer.start(content.getContentAsText()); int tokenIdx = 0; while (filterLexer.getTokenType() != null) { diff --git a/platform/indexing-impl/src/com/intellij/psi/impl/cache/impl/id/IdDataConsumer.java b/platform/indexing-impl/src/com/intellij/psi/impl/cache/impl/id/IdDataConsumer.java index d97193c5a22e..655302e1f921 100644 --- a/platform/indexing-impl/src/com/intellij/psi/impl/cache/impl/id/IdDataConsumer.java +++ b/platform/indexing-impl/src/com/intellij/psi/impl/cache/impl/id/IdDataConsumer.java @@ -104,7 +104,6 @@ public final class IdDataConsumer { myOccurrences.forEach((hash, value) -> consumer.accept(new IdIndexEntry(hash), value)); } }; - public @NotNull Map getResult() { return myResult; } diff --git a/platform/indexing-impl/src/com/intellij/psi/impl/search/PsiSearchHelperImpl.java b/platform/indexing-impl/src/com/intellij/psi/impl/search/PsiSearchHelperImpl.java index 02475c668102..16226a59cfb7 100644 --- a/platform/indexing-impl/src/com/intellij/psi/impl/search/PsiSearchHelperImpl.java +++ b/platform/indexing-impl/src/com/intellij/psi/impl/search/PsiSearchHelperImpl.java @@ -31,6 +31,7 @@ import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.util.text.TrigramBuilder; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.platform.indexing.BinaryFileSourceProvider; import com.intellij.psi.*; import com.intellij.psi.impl.PsiManagerEx; import com.intellij.psi.impl.cache.CacheManager; @@ -370,7 +371,7 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { try { progress.setText(IndexingBundle.message("psi.scanning.files.progress")); - Processor localProcessor = localProcessor(searcher, processor); + Processor localProcessor = localProcessor(searcher, processor); // Lists of files to search in this order. // First, there are lists with higher probability of hits (e.g., files with `containerName` or files near the target) @@ -449,6 +450,9 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { } /** + * NOTE: candidateFile and file might be actually 2 different files (e.g. we may find class file in java, but the PsiFile + * will be from mirror source class) + * * @param files to scan for references in this pass. * @param totalSize the number of files to scan in both passes. Can be different from {@code files.size()} in case of * two-pass scan, where we first scan files containing container name and then all the rest files. @@ -459,7 +463,7 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { int totalSize, int alreadyProcessedFiles, @NotNull ProgressIndicator progress, - @NotNull Processor localProcessor) { + @NotNull Processor localProcessor) { return myManager.runInBatchFilesMode(() -> { AtomicInteger counter = new AtomicInteger(alreadyProcessedFiles); AtomicBoolean stopped = new AtomicBoolean(false); @@ -488,6 +492,15 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { }); } + /** + * NOTE: candidateFile and file might be actually two different files (e.g. we may find class file in java, but the PsiFile + * will be from mirror source class) + */ + record CandidateFileInfo( + VirtualFile candidateFile, + PsiFile file + ) { } + // Tries to run {@code localProcessor} for each file in {@code files} concurrently on ForkJoinPool. // When encounters write action request, stops all threads, waits for write action to finish and re-starts all threads again, // trying to finish the unprocessed files (i.e. those for which {@code localProcessor} hasn't been called yet). @@ -590,7 +603,7 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { private void processVirtualFile(@NotNull VirtualFile vfile, @NotNull AtomicBoolean stopped, - @NotNull Processor localProcessor) throws ApplicationUtil.CannotRunReadActionException { + @NotNull Processor localProcessor) throws ApplicationUtil.CannotRunReadActionException { // try to pre-cache virtual file content outside read action to avoid stalling EDT if (!vfile.isDirectory() && !vfile.getFileType().isBinary()) { try { @@ -608,6 +621,13 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { file = ((BackFileViewProvider)file.getViewProvider()).getFrontPsiFile(); } + if (file instanceof PsiBinaryFile binaryFile) { + PsiFile originalFile = findOriginalFile(binaryFile); + if (originalFile != null) { + file = originalFile; + } + } + if (file != null && !(file instanceof PsiBinaryFile)) { Project project = myManager.getProject(); if (project.isDisposed()) throw new ProcessCanceledException(); @@ -627,7 +647,7 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { continue; } - if (!localProcessor.process(psiRoot)) { + if (!localProcessor.process(new CandidateFileInfo(vfile, psiRoot))) { stopped.set(true); break; } @@ -638,6 +658,16 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { } } + @Nullable + private static PsiFile findOriginalFile(@NotNull PsiBinaryFile file) { + List providers = BinaryFileSourceProvider.EP.getExtensionList(); + for (BinaryFileSourceProvider provider : providers) { + PsiFile originalFile = provider.findSourceFile(file); + if (originalFile != null) return originalFile; + } + return null; + } + private void getFilesWithText(@NotNull GlobalSearchScope scope, short searchContext, boolean caseSensitively, @@ -793,7 +823,7 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { Map> globals = new HashMap<>(); List> customs = new ArrayList<>(); Set locals = new LinkedHashSet<>(); - Map> localProcessors = new HashMap<>(); + Map> localProcessors = new HashMap<>(); distributePrimitives(collectors, locals, globals, customs, localProcessors); if (!processGlobalRequestsOptimized(globals, progress, localProcessors)) { return false; @@ -834,10 +864,11 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { CHANGED, } - static @NotNull Processor localProcessor(@NotNull StringSearcher searcher, @NotNull BulkOccurrenceProcessor processor) { + static @NotNull Processor localProcessor(@NotNull StringSearcher searcher, @NotNull BulkOccurrenceProcessor processor) { return new ReadActionProcessor<>() { @Override - public boolean processInReadAction(PsiElement scopeElement) { + public boolean processInReadAction(CandidateFileInfo candidateFileInfo) { + PsiElement scopeElement = candidateFileInfo.file(); if (scopeElement instanceof PsiCompiledElement) { // can't scan text of the element return true; @@ -856,7 +887,7 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { private boolean processGlobalRequestsOptimized(@NotNull Map> singles, @NotNull ProgressIndicator progress, - @NotNull Map> localProcessors) { + @NotNull Map> localProcessors) { if (singles.isEmpty()) { return true; } @@ -874,7 +905,7 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { boolean processGlobalRequests(@NotNull Map> singles, @NotNull ProgressIndicator progress, - @NotNull Map> localProcessors) { + @NotNull Map> localProcessors) { progress.pushState(); progress.setText(IndexingBundle.message("psi.scanning.files.progress")); boolean result; @@ -926,22 +957,22 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { return result; } - private boolean processCandidates(@NotNull Map> localProcessors, + private boolean processCandidates(@NotNull Map> localProcessors, @NotNull Map> candidateFiles, @NotNull ProgressIndicator progress, int totalSize, int alreadyProcessedFiles) { List files = new ArrayList<>(candidateFiles.keySet()); - return processPsiFileRoots(files, totalSize, alreadyProcessedFiles, progress, psiRoot -> { - VirtualFile vfile = psiRoot.getVirtualFile(); + return processPsiFileRoots(files, totalSize, alreadyProcessedFiles, progress, candidateInfo -> { + VirtualFile vfile = candidateInfo.candidateFile(); if (vfile instanceof BackedVirtualFile) { vfile = ((BackedVirtualFile)vfile).getOriginFile(); } for (T singleRequest : candidateFiles.get(vfile)) { ProgressManager.checkCanceled(); - Processor localProcessor = localProcessors.get(singleRequest); - if (!localProcessor.process(psiRoot)) { + Processor localProcessor = localProcessors.get(singleRequest); + if (!localProcessor.process(candidateInfo)) { return false; } } @@ -1090,7 +1121,7 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { @NotNull Set locals, @NotNull Map> globals, @NotNull List> customs, - @NotNull Map> localProcessors) { + @NotNull Map> localProcessors) { for (Map.Entry> entry : collectors.entrySet()) { ProgressManager.checkCanceled(); Processor processor = entry.getValue(); @@ -1120,9 +1151,9 @@ public class PsiSearchHelperImpl implements PsiSearchHelper { StringSearcher searcher = new StringSearcher(primitive.word, primitive.caseSensitive, true, false); BulkOccurrenceProcessor adapted = adaptProcessor(primitive, singleRequest.refProcessor); - Processor localProcessor = localProcessor(searcher, adapted); + Processor localProcessor = localProcessor(searcher, adapted); - Processor old = localProcessors.put(singleRequest, localProcessor); + Processor old = localProcessors.put(singleRequest, localProcessor); assert old == null : old + ";" + localProcessor +"; singleRequest="+singleRequest; } } diff --git a/platform/indexing-impl/src/com/intellij/psi/impl/search/helper.kt b/platform/indexing-impl/src/com/intellij/psi/impl/search/helper.kt index 3a1662c328c7..f720e30826f9 100644 --- a/platform/indexing-impl/src/com/intellij/psi/impl/search/helper.kt +++ b/platform/indexing-impl/src/com/intellij/psi/impl/search/helper.kt @@ -9,7 +9,7 @@ import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project import com.intellij.openapi.util.ClassExtension import com.intellij.openapi.util.Computable -import com.intellij.psi.PsiElement +import com.intellij.psi.impl.search.PsiSearchHelperImpl.CandidateFileInfo import com.intellij.psi.search.LocalSearchScope import com.intellij.psi.search.PsiSearchHelper import com.intellij.psi.search.SearchSession @@ -225,8 +225,8 @@ private class Layer( return myHelper.processGlobalRequests(globalsIds, progress, scopeProcessors(globals)) } - private fun scopeProcessors(globals: Collection): Map> { - val result = HashMap>() + private fun scopeProcessors(globals: Collection): Map> { + val result = HashMap>() for (requestAndProcessors: RequestAndProcessors in globals) { progress.checkCanceled() result[requestAndProcessors.request] = scopeProcessor(requestAndProcessors) @@ -234,7 +234,7 @@ private class Layer( return result } - private fun scopeProcessor(requestAndProcessors: RequestAndProcessors): Processor { + private fun scopeProcessor(requestAndProcessors: RequestAndProcessors): Processor { val (request: WordRequestInfo, processors: RequestProcessors) = requestAndProcessors val searcher = StringSearcher(request.word, request.isCaseSensitive, true, false) val adapted = MyBulkOccurrenceProcessor(project, processors) diff --git a/platform/util/api-dump-unreviewed.txt b/platform/util/api-dump-unreviewed.txt index 7bc094c876e1..9bd8d90c88a3 100644 --- a/platform/util/api-dump-unreviewed.txt +++ b/platform/util/api-dump-unreviewed.txt @@ -1973,6 +1973,7 @@ c:com.intellij.openapi.util.text.StringUtil - s:isEmptyOrSpaces(java.lang.String):Z - s:isEscapedBackslash(java.lang.CharSequence,I,I):Z - s:isHexDigit(C):Z +- s:isJavaIdentifier(java.lang.CharSequence,I,I):Z - s:isJavaIdentifier(java.lang.String):Z - s:isJavaIdentifierPart(C):Z - s:isJavaIdentifierStart(C):Z diff --git a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java index 19b00df554f7..625ddbc9544d 100644 --- a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java +++ b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java @@ -11,6 +11,7 @@ import com.intellij.util.*; import com.intellij.util.text.CharArrayUtil; import com.intellij.util.text.CharSequenceSubSequence; import com.intellij.util.text.MergingCharSequence; +import kotlin.jvm.internal.Ref; import org.jetbrains.annotations.*; import org.jetbrains.annotations.ApiStatus.NonExtendable; @@ -2275,6 +2276,9 @@ public class StringUtil { return cp >= '0' && cp <= '9' || cp >= 'a' && cp <= 'z' || cp >= 'A' && cp <= 'Z' || Character.isJavaIdentifierPart(cp); } + /** + * @return true iff the string is a valid java identifier (according to JLS 3.8) + */ @Contract(pure = true) public static boolean isJavaIdentifier(@NotNull String text) { int len = text.length(); @@ -2291,6 +2295,58 @@ public class StringUtil { return true; } + /** + * Checks if the char sequence slice is a valid java identifier (according to JLS 3.8) + * @return true exactly iff {@code isJavaIdentifier(str.subSequence(startIncl, endExcl)) == true } + */ + public static boolean isJavaIdentifier(@NotNull CharSequence str, int startIncl, int endExcl) { + if (str.length() == 0) return false; + Ref.BooleanRef isFirst = new Ref.BooleanRef(); + isFirst.element = true; + return processCodePoints(str, startIncl, endExcl, codePoint -> { + if (isFirst.element) { + isFirst.element = false; + if (!isJavaIdentifierStart(codePoint)) { + return false; + } + } else { + if (!isJavaIdentifierPart(codePoint)) { + return false; + } + } + return true; + }); + } + + private interface CodePointProcessor { + /** + * @return true iff should continue + */ + boolean acceptCodePoint(int codePoint); + } + + + private static boolean processCodePoints(@NotNull CharSequence str, int startIncl, int endExcl, CodePointProcessor processor) { + int i = startIncl; + while (i < endExcl) { + char c1 = str.charAt(i++); + if (!Character.isHighSurrogate(c1) || i >= endExcl) { + if (!processor.acceptCodePoint(c1)) return false; + } + else { + char c2 = str.charAt(i); + if (Character.isLowSurrogate(c2)) { + i++; + if (!processor.acceptCodePoint(Character.toCodePoint(c1, c2))) return false; + } + else { + if (!processor.acceptCodePoint(c2)) return false; + } + } + } + return true; + } + /** * Escape property name or key in property file. Unicode characters are escaped as well. * diff --git a/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java b/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java index b051c93ba4b9..27ab09d678cb 100644 --- a/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java +++ b/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java @@ -943,6 +943,23 @@ public class StringUtilTest { assertTrue(StringUtil.isJavaIdentifier("\u03B1A")); } + @SuppressWarnings("UnnecessaryUnicodeEscape") + @Test + public void testCharSequenceSliceIsJavaIdentifier() { + assertFalse(StringUtil.isJavaIdentifier("", 0, 0)); + assertTrue(StringUtil.isJavaIdentifier("x", 0, 1)); + assertFalse(StringUtil.isJavaIdentifier("0", 0, 1)); + assertFalse(StringUtil.isJavaIdentifier("0x", 0, 2)); + assertTrue(StringUtil.isJavaIdentifier("foo$bar", 0, 7)); + assertTrue(StringUtil.isJavaIdentifier("x0", 0, 2)); + assertTrue(StringUtil.isJavaIdentifier("\uD835\uDEFCA", 0, 3)); + assertTrue(StringUtil.isJavaIdentifier("A\uD835\uDEFC", 0, 3)); + assertTrue(StringUtil.isJavaIdentifier("\u03B1A", 0, 2)); + assertTrue(StringUtil.isJavaIdentifier("###\u03B1A", 3, 5)); + assertTrue(StringUtil.isJavaIdentifier("\u03B1A###", 0, 2)); + assertTrue(StringUtil.isJavaIdentifier("###\u03B1A###", 3, 5)); + } + @Test public void testSplit() { String spaceSeparator = " ";