[java] implement constant pool parser and build id index using it for class files in jars IDEA-327594

GitOrigin-RevId: fff7c155b86e950e547fd8d01f142867c03c7c5c
This commit is contained in:
Roman Ivanov
2024-04-19 14:19:21 +00:00
committed by intellij-monorepo-bot
parent 5cca49b4f1
commit 5887fe8221
35 changed files with 1093 additions and 30 deletions
+8 -2
View File
@@ -306,6 +306,8 @@
<notificationGroup id="Infer Nullity" displayType="TOOL_WINDOW" toolWindowId="Infer Nullity" bundle="messages.JavaBundle" key="dialog.title.infer.nullity"/>
<notificationGroup id="JDK Arch Check" displayType="STICKY_BALLOON" bundle="messages.JavaBundle" key="notification.group.arch.checker"/>
<registryKey key="index.ids.from.java.sources.in.jar" defaultValue="true" description="Index ids from class files instead of source files in JARs" restartRequired="false"/>
<registryKey key="ide.jps.use.build.tool.window" defaultValue="true" description="Enables 'Build' toolwindow for JPS compilation messages"/>
<registryKey key="java.jps.backward.ref.index.builder.fs.case.sensitive" defaultValue="BY_ROOT"
description="Case sensitivity of the project file system for compiler references. Available values: SENSITIVE, INSENSITIVE, BY_OS (Defined by OS or VM options), BY_ROOT (Defined by first root)"/>
@@ -1007,6 +1009,7 @@
<highlightVisitor implementation="com.intellij.codeInsight.daemon.JavaRainbowVisitor"/>
<problemHighlightFilter implementation="com.intellij.codeInsight.daemon.JavaProblemHighlightFilter"/>
<todoIndexer filetype="JAVA" implementationClass="com.intellij.psi.impl.cache.impl.idCache.JavaTodoIndexer"/>
<idIndexer filetype="CLASS" implementationClass="com.intellij.psi.impl.cache.impl.idCache.JavaIdIndexer"/>
<idIndexer filetype="JAVA" implementationClass="com.intellij.psi.impl.cache.impl.idCache.JavaIdIndexer"/>
<fileIconPatcher id="javaFileIconPatcher" implementation="com.intellij.ide.JavaFileIconPatcher"/>
<basicWordSelectionFilter implementation="com.intellij.codeInsight.editorActions.wordSelection.JavaBasicWordSelectionFilter"/>
@@ -1267,6 +1270,7 @@
<enterHandlerDelegate implementation="com.intellij.codeInsight.editorActions.JavaEnterInTextBlockHandler"/>
<breadcrumbsInfoProvider implementation="com.intellij.lang.java.JavaBreadcrumbsInfoProvider"/>
<editorFileSwapper implementation="com.intellij.codeEditor.JavaEditorFileSwapper"/>
<binaryFileSourceProvider implementation="com.intellij.codeEditor.JavaBinaryFileSourceProvider" />
<flipCommaIntention.flipper language="JAVA" implementationClass="com.intellij.codeInsight.intention.impl.JavaFlipper"/>
<listSplitJoinContext language="JAVA" implementationClass="com.intellij.codeInsight.intention.impl.lists.JavaSplitJoinArgumentsContext"/>
<listSplitJoinContext language="JAVA" implementationClass="com.intellij.codeInsight.intention.impl.lists.JavaSplitJoinParametersContext"/>
@@ -2515,8 +2519,8 @@
<pluginSuggestionProvider implementation="com.intellij.ide.ant.AntSuggestionProvider"/>
<pluginSuggestionProvider implementation="com.intellij.ide.android.AndroidSuggestionProvider"/>
<statistics.counterUsagesCollector
implementationClass="com.intellij.codeInsight.generation.analysis.GenerateLoggerStatisticsCollector"/>
<statistics.counterUsagesCollector
implementationClass="com.intellij.codeInsight.generation.analysis.GenerateLoggerStatisticsCollector"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains">
@@ -2550,6 +2554,8 @@
<applicationListeners>
<listener class="com.intellij.codeInsight.daemon.problems.pass.ProjectProblemFileFileEditorManagerListener"
topic="com.intellij.openapi.fileEditor.FileEditorManagerListener"/>
<listener class="com.intellij.psi.impl.cache.impl.idCache.JavaIdIndexRegistryValueListener"
topic="com.intellij.openapi.util.registry.RegistryValueListener" />
</applicationListeners>
<projectListeners>
@@ -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);
}
}
@@ -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"));
}
}
@@ -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)
}
}
}
@@ -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<IdIndexEntry, Integer> map(@NotNull FileContent inputData) {
VirtualFile file = inputData.getFile();
String extension = file.getExtension();
if (extension != null && extension.equals(JavaClassFileType.DEFAULT_EXTENSION) && isEnabled) {
Map<IdIndexEntry, Integer> 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<IdIndexEntry, Integer> 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) {
@@ -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<Ljava/util/List<+Ljava/util/Comparator<Ljava/lang/String;>;>;>;>;
// 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;
}
}
@@ -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<String[], List<List<? extends Comparator<String>>>> 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<? extends CharSequence> 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<? super CharSequence> 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 <TYPE_PARAM> 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 <TYPE_PARAM extends CharSequence> 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 <TYPE_PARAM extends CharSequence> {}
* ```
*/
@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<T extends Comparable<String> & List<Integer>> {
* }
* ```
*/
@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 {
* <T extends CharSequence & Comparable<Integer>>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<String> {
val parts = ArrayList<CharSequence>()
ConstantPoolParser(DataInputStream(ByteArrayInputStream(bytes))) {
val tmp = ArrayList<CharSequence>()
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
}
@@ -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("<T::Ljava/lang/Comparable<Ljava/lang/String;>;:Ljava/util/List<Ljava/lang/Integer;>;>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<Ljava/util/List<+Ljava/util/Comparator<Ljava/lang/String;>;>;>;>;"));
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<String> getJvmIdParts(String str) {
ArrayList<CharSequence> 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;
}
}
@@ -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
@@ -22,6 +22,7 @@
</extensionPoint>
<extensionPoint name="findModelExtension" interface="com.intellij.find.FindModelExtension" dynamic="true"/>
<extensionPoint name="codeUsageScopeOptimizer" interface="com.intellij.psi.search.ScopeOptimizer" dynamic="true"/>
<extensionPoint name="binaryFileSourceProvider" interface="com.intellij.platform.indexing.BinaryFileSourceProvider" dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
@@ -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<BinaryFileSourceProvider> 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);
}
@@ -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) {
@@ -104,7 +104,6 @@ public final class IdDataConsumer {
myOccurrences.forEach((hash, value) -> consumer.accept(new IdIndexEntry(hash), value));
}
};
public @NotNull Map<IdIndexEntry, Integer> getResult() {
return myResult;
}
@@ -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<PsiElement> localProcessor = localProcessor(searcher, processor);
Processor<CandidateFileInfo> 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<? super PsiFile> localProcessor) {
@NotNull Processor<CandidateFileInfo> 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<? super PsiFile> localProcessor) throws ApplicationUtil.CannotRunReadActionException {
@NotNull Processor<CandidateFileInfo> 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<BinaryFileSourceProvider> 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<TextIndexQuery, Collection<RequestWithProcessor>> globals = new HashMap<>();
List<Computable<Boolean>> customs = new ArrayList<>();
Set<RequestWithProcessor> locals = new LinkedHashSet<>();
Map<RequestWithProcessor, Processor<? super PsiElement>> localProcessors = new HashMap<>();
Map<RequestWithProcessor, Processor<CandidateFileInfo>> 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<PsiElement> localProcessor(@NotNull StringSearcher searcher, @NotNull BulkOccurrenceProcessor processor) {
static @NotNull Processor<CandidateFileInfo> 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<TextIndexQuery, Collection<RequestWithProcessor>> singles,
@NotNull ProgressIndicator progress,
@NotNull Map<RequestWithProcessor, Processor<? super PsiElement>> localProcessors) {
@NotNull Map<RequestWithProcessor, Processor<CandidateFileInfo>> localProcessors) {
if (singles.isEmpty()) {
return true;
}
@@ -874,7 +905,7 @@ public class PsiSearchHelperImpl implements PsiSearchHelper {
<T extends WordRequestInfo> boolean processGlobalRequests(@NotNull Map<TextIndexQuery, Collection<T>> singles,
@NotNull ProgressIndicator progress,
@NotNull Map<T, Processor<? super PsiElement>> localProcessors) {
@NotNull Map<T, Processor<CandidateFileInfo>> 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 <T> boolean processCandidates(@NotNull Map<T, Processor<? super PsiElement>> localProcessors,
private <T> boolean processCandidates(@NotNull Map<T, Processor<CandidateFileInfo>> localProcessors,
@NotNull Map<VirtualFile, Collection<T>> candidateFiles,
@NotNull ProgressIndicator progress,
int totalSize,
int alreadyProcessedFiles) {
List<VirtualFile> 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<? super PsiElement> localProcessor = localProcessors.get(singleRequest);
if (!localProcessor.process(psiRoot)) {
Processor<CandidateFileInfo> localProcessor = localProcessors.get(singleRequest);
if (!localProcessor.process(candidateInfo)) {
return false;
}
}
@@ -1090,7 +1121,7 @@ public class PsiSearchHelperImpl implements PsiSearchHelper {
@NotNull Set<RequestWithProcessor> locals,
@NotNull Map<TextIndexQuery, Collection<RequestWithProcessor>> globals,
@NotNull List<? super Computable<Boolean>> customs,
@NotNull Map<RequestWithProcessor, Processor<? super PsiElement>> localProcessors) {
@NotNull Map<RequestWithProcessor, Processor<CandidateFileInfo>> localProcessors) {
for (Map.Entry<SearchRequestCollector, Processor<? super PsiReference>> entry : collectors.entrySet()) {
ProgressManager.checkCanceled();
Processor<? super PsiReference> 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<PsiElement> localProcessor = localProcessor(searcher, adapted);
Processor<CandidateFileInfo> localProcessor = localProcessor(searcher, adapted);
Processor<? super PsiElement> old = localProcessors.put(singleRequest, localProcessor);
Processor<CandidateFileInfo> old = localProcessors.put(singleRequest, localProcessor);
assert old == null : old + ";" + localProcessor +"; singleRequest="+singleRequest;
}
}
@@ -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<T>(
return myHelper.processGlobalRequests(globalsIds, progress, scopeProcessors(globals))
}
private fun scopeProcessors(globals: Collection<RequestAndProcessors>): Map<WordRequestInfo, Processor<in PsiElement>> {
val result = HashMap<WordRequestInfo, Processor<in PsiElement>>()
private fun scopeProcessors(globals: Collection<RequestAndProcessors>): Map<WordRequestInfo, Processor<CandidateFileInfo>> {
val result = HashMap<WordRequestInfo, Processor<CandidateFileInfo>>()
for (requestAndProcessors: RequestAndProcessors in globals) {
progress.checkCanceled()
result[requestAndProcessors.request] = scopeProcessor(requestAndProcessors)
@@ -234,7 +234,7 @@ private class Layer<T>(
return result
}
private fun scopeProcessor(requestAndProcessors: RequestAndProcessors): Processor<in PsiElement> {
private fun scopeProcessor(requestAndProcessors: RequestAndProcessors): Processor<CandidateFileInfo> {
val (request: WordRequestInfo, processors: RequestProcessors) = requestAndProcessors
val searcher = StringSearcher(request.word, request.isCaseSensitive, true, false)
val adapted = MyBulkOccurrenceProcessor(project, processors)
+1
View File
@@ -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
@@ -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.
*
@@ -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 = " ";