├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── build.yml │ ├── release.yml │ └── run-ui-tests.yml ├── .gitignore ├── .run ├── Run IDE for UI Tests.run.xml ├── Run IDE with Plugin.run.xml ├── Run Plugin Tests.run.xml ├── Run Plugin Verification.run.xml └── Run Qodana.run.xml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── detekt-config.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── qodana.yml ├── settings.gradle.kts └── src └── main ├── java └── net │ └── earthcomputer │ └── classfileindexer │ └── MyAgent.java ├── kotlin └── net │ └── earthcomputer │ └── classfileindexer │ ├── AgentInitializedListener.kt │ ├── BinaryIndexKey.kt │ ├── ClassFileIndex.kt │ ├── ClassFileIndexExtension.kt │ ├── ClassLocator.kt │ ├── DecompiledSourceElementLocator.kt │ ├── FakeDecompiledElement.kt │ ├── FieldLocator.kt │ ├── IHasCustomDescription.kt │ ├── IHasNavigationOffset.kt │ ├── IIsWriteOverride.kt │ ├── ImplicitToStringLocator.kt │ ├── ImplicitToStringSearchExtension.kt │ ├── IndexerAnnotationVisitor.kt │ ├── IndexerClassVisitor.kt │ ├── IndexerFieldVisitor.kt │ ├── IndexerMethodVisitor.kt │ ├── IndexerRecordComponentVisitor.kt │ ├── MethodLocator.kt │ ├── MethodReferencesSearchExtension.kt │ ├── ReferencesSearchExtension.kt │ ├── SmartMap.kt │ └── utils.kt └── resources └── META-INF ├── plugin.xml └── pluginIcon.svg /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 12 | 13 | **Describe the bug:** 14 | 15 | 16 | **Steps to reproduce:** 17 | 18 | 19 | **Expected behavior:** 20 | 21 | 22 | **Additional context:** 23 | 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for Gradle dependencies 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | target-branch: "main" 10 | schedule: 11 | interval: "daily" 12 | # Maintain dependencies for GitHub Actions 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | target-branch: "main" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for testing and preparing the plugin release in following steps: 2 | # - validate Gradle Wrapper, 3 | # - run 'test' and 'verifyPlugin' tasks, 4 | # - run Qodana inspections, 5 | # - run 'buildPlugin' task and prepare artifact for the further tests, 6 | # - run 'runPluginVerifier' task, 7 | # - create a draft release. 8 | # 9 | # Workflow is triggered on push and pull_request events. 10 | # 11 | # GitHub Actions reference: https://help.github.com/en/actions 12 | # 13 | ## JBIJPPTPL 14 | 15 | name: Build 16 | on: 17 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests) 18 | push: 19 | branches: [main] 20 | # Trigger the workflow on any pull request 21 | pull_request: 22 | 23 | jobs: 24 | 25 | # Run Gradle Wrapper Validation Action to verify the wrapper's checksum 26 | # Run verifyPlugin, IntelliJ Plugin Verifier, and test Gradle tasks 27 | # Build plugin and provide the artifact for the next workflow jobs 28 | build: 29 | name: Build 30 | runs-on: ubuntu-latest 31 | outputs: 32 | version: ${{ steps.properties.outputs.version }} 33 | changelog: ${{ steps.properties.outputs.changelog }} 34 | steps: 35 | 36 | # Check out current repository 37 | - name: Fetch Sources 38 | uses: actions/checkout@v3 39 | 40 | # Validate wrapper 41 | - name: Gradle Wrapper Validation 42 | uses: gradle/wrapper-validation-action@v1.0.4 43 | 44 | # Setup Java 11 environment for the next steps 45 | - name: Setup Java 46 | uses: actions/setup-java@v3 47 | with: 48 | distribution: zulu 49 | java-version: 11 50 | cache: gradle 51 | 52 | # Set environment variables 53 | - name: Export Properties 54 | id: properties 55 | shell: bash 56 | run: | 57 | PROPERTIES="$(./gradlew properties --console=plain -q)" 58 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 59 | NAME="$(echo "$PROPERTIES" | grep "^pluginName:" | cut -f2- -d ' ')" 60 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 61 | CHANGELOG="${CHANGELOG//'%'/'%25'}" 62 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 63 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" 64 | 65 | echo "::set-output name=version::$VERSION" 66 | echo "::set-output name=name::$NAME" 67 | echo "::set-output name=changelog::$CHANGELOG" 68 | echo "::set-output name=pluginVerifierHomeDir::~/.pluginVerifier" 69 | 70 | ./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier 71 | 72 | # Run tests 73 | - name: Run Tests 74 | run: ./gradlew test 75 | 76 | # Collect Tests Result of failed tests 77 | - name: Collect Tests Result 78 | if: ${{ failure() }} 79 | uses: actions/upload-artifact@v3 80 | with: 81 | name: tests-result 82 | path: ${{ github.workspace }}/build/reports/tests 83 | 84 | # Cache Plugin Verifier IDEs 85 | - name: Setup Plugin Verifier IDEs Cache 86 | uses: actions/cache@v3 87 | with: 88 | path: ${{ steps.properties.outputs.pluginVerifierHomeDir }}/ides 89 | key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} 90 | 91 | # Run Verify Plugin task and IntelliJ Plugin Verifier tool 92 | - name: Run Plugin Verification tasks 93 | run: ./gradlew runPluginVerifier -Pplugin.verifier.home.dir=${{ steps.properties.outputs.pluginVerifierHomeDir }} 94 | 95 | # Collect Plugin Verifier Result 96 | - name: Collect Plugin Verifier Result 97 | if: ${{ always() }} 98 | uses: actions/upload-artifact@v3 99 | with: 100 | name: pluginVerifier-result 101 | path: ${{ github.workspace }}/build/reports/pluginVerifier 102 | 103 | # Run Qodana inspections 104 | # - name: Qodana - Code Inspection 105 | # uses: JetBrains/qodana-action@v5.1.0 106 | 107 | # Prepare plugin archive content for creating artifact 108 | - name: Prepare Plugin Artifact 109 | id: artifact 110 | shell: bash 111 | run: | 112 | cd ${{ github.workspace }}/build/distributions 113 | FILENAME=`ls *.zip` 114 | unzip "$FILENAME" -d content 115 | 116 | echo "::set-output name=filename::${FILENAME:0:-4}" 117 | 118 | # Store already-built plugin as an artifact for downloading 119 | - name: Upload artifact 120 | uses: actions/upload-artifact@v3 121 | with: 122 | name: ${{ steps.artifact.outputs.filename }} 123 | path: ./build/distributions/content/*/* 124 | 125 | # Prepare a draft release for GitHub Releases page for the manual verification 126 | # If accepted and published, release workflow would be triggered 127 | releaseDraft: 128 | name: Release Draft 129 | if: github.event_name != 'pull_request' 130 | needs: build 131 | runs-on: ubuntu-latest 132 | permissions: 133 | contents: write 134 | steps: 135 | 136 | # Check out current repository 137 | - name: Fetch Sources 138 | uses: actions/checkout@v3 139 | 140 | # Remove old release drafts by using the curl request for the available releases with draft flag 141 | - name: Remove Old Release Drafts 142 | env: 143 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 144 | run: | 145 | gh api repos/{owner}/{repo}/releases \ 146 | --jq '.[] | select(.draft == true) | .id' \ 147 | | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 148 | 149 | # Create new release draft - which is not publicly visible and requires manual acceptance 150 | - name: Create Release Draft 151 | env: 152 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 153 | run: | 154 | gh release create v${{ needs.build.outputs.version }} \ 155 | --draft \ 156 | --title "v${{ needs.build.outputs.version }}" \ 157 | --notes "$(cat << 'EOM' 158 | ${{ needs.build.outputs.changelog }} 159 | EOM 160 | )" 161 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared 2 | # with the Build workflow. Running the publishPlugin task requires the PUBLISH_TOKEN secret provided. 3 | 4 | name: Release 5 | on: 6 | release: 7 | types: [prereleased, released] 8 | 9 | jobs: 10 | 11 | # Prepare and publish the plugin to the Marketplace repository 12 | release: 13 | name: Publish Plugin 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | steps: 19 | 20 | # Check out current repository 21 | - name: Fetch Sources 22 | uses: actions/checkout@v3 23 | with: 24 | ref: ${{ github.event.release.tag_name }} 25 | 26 | # Setup Java 11 environment for the next steps 27 | - name: Setup Java 28 | uses: actions/setup-java@v3 29 | with: 30 | distribution: zulu 31 | java-version: 11 32 | cache: gradle 33 | 34 | # Set environment variables 35 | - name: Export Properties 36 | id: properties 37 | shell: bash 38 | run: | 39 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 40 | ${{ github.event.release.body }} 41 | EOM 42 | )" 43 | 44 | CHANGELOG="${CHANGELOG//'%'/'%25'}" 45 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 46 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" 47 | 48 | echo "::set-output name=changelog::$CHANGELOG" 49 | 50 | # Update Unreleased section with the current release note 51 | - name: Patch Changelog 52 | if: ${{ steps.properties.outputs.changelog != '' }} 53 | env: 54 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 55 | run: | 56 | ./gradlew patchChangelog --release-note="$CHANGELOG" 57 | 58 | # Publish the plugin to the Marketplace 59 | - name: Publish Plugin 60 | env: 61 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 62 | run: ./gradlew publishPlugin 63 | 64 | # Upload artifact as a release asset 65 | - name: Upload Release Asset 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 69 | 70 | # Create pull request 71 | - name: Create Pull Request 72 | if: ${{ steps.properties.outputs.changelog != '' }} 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | run: | 76 | VERSION="${{ github.event.release.tag_name }}" 77 | BRANCH="changelog-update-$VERSION" 78 | 79 | git config user.email "action@github.com" 80 | git config user.name "GitHub Action" 81 | 82 | git checkout -b $BRANCH 83 | git commit -am "Changelog update - $VERSION" 84 | git push --set-upstream origin $BRANCH 85 | 86 | gh pr create \ 87 | --title "Changelog update - \`$VERSION\`" \ 88 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 89 | --base main \ 90 | --head $BRANCH 91 | -------------------------------------------------------------------------------- /.github/workflows/run-ui-tests.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps: 2 | # - prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with UI 3 | # - wait for IDE to start 4 | # - run UI tests with separate Gradle task 5 | # 6 | # Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform 7 | # 8 | # Workflow is triggered manually. 9 | 10 | name: Run UI Tests 11 | on: 12 | workflow_dispatch 13 | 14 | jobs: 15 | 16 | testUI: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - os: ubuntu-latest 23 | runIde: | 24 | export DISPLAY=:99.0 25 | Xvfb -ac :99 -screen 0 1920x1080x16 & 26 | gradle runIdeForUiTests & 27 | - os: windows-latest 28 | runIde: start gradlew.bat runIdeForUiTests 29 | - os: macos-latest 30 | runIde: ./gradlew runIdeForUiTests & 31 | 32 | steps: 33 | 34 | # Check out current repository 35 | - name: Fetch Sources 36 | uses: actions/checkout@v3 37 | 38 | # Setup Java 11 environment for the next steps 39 | - name: Setup Java 40 | uses: actions/setup-java@v3 41 | with: 42 | distribution: zulu 43 | java-version: 11 44 | cache: gradle 45 | 46 | # Run IDEA prepared for UI testing 47 | - name: Run IDE 48 | run: ${{ matrix.runIde }} 49 | 50 | # Wait for IDEA to be started 51 | - name: Health Check 52 | uses: jtalk/url-health-check-action@v2 53 | with: 54 | url: http://127.0.0.1:8082 55 | max-attempts: 15 56 | retry-delay: 30s 57 | 58 | # Run tests 59 | - name: Tests 60 | run: ./gradlew test 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | .qodana 4 | build 5 | -------------------------------------------------------------------------------- /.run/Run IDE for UI Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 15 | 17 | true 18 | true 19 | false 20 | 21 | 22 | -------------------------------------------------------------------------------- /.run/Run IDE with Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Run Plugin Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.run/Run Plugin Verification.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 25 | 26 | -------------------------------------------------------------------------------- /.run/Run Qodana.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 16 | 19 | 21 | true 22 | true 23 | false 24 | 25 | 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # class-file-indexer Changelog 4 | 5 | ## [Unreleased] 6 | ### Added 7 | - Support for 2022.1 8 | ### Changed 9 | - Update dependencies 10 | 11 | ## [1.1.1] 12 | ### Added 13 | - Support for 2021.3 14 | 15 | ## [1.1.0] 16 | ### Changed 17 | - Improved performance 18 | 19 | ## [1.0.0] 20 | ### Added 21 | - Allow find usages in libraries without sources 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Joseph Burton 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # class-file-indexer 2 | 3 | ![Build](https://github.com/Earthcomputer/class-file-indexer/workflows/Build/badge.svg) 4 | [![Version](https://img.shields.io/jetbrains/plugin/v/PLUGIN_ID.svg)](https://plugins.jetbrains.com/plugin/PLUGIN_ID) 5 | [![Downloads](https://img.shields.io/jetbrains/plugin/d/PLUGIN_ID.svg)](https://plugins.jetbrains.com/plugin/PLUGIN_ID) 6 | 7 | 8 | Allow find usages in libraries without sources. 9 | Normally IntelliJ's find usages action does not include results from decompiled library sources. This plugin fixes that. Simply install and you're done! 10 | 11 | 12 | ## Installation 13 | 14 | - Using IDE built-in plugin system: 15 | 16 | Settings/Preferences > Plugins > Marketplace > Search for "Class File Indexer" > 17 | Install Plugin 18 | 19 | - Manually: 20 | 21 | Download the [latest release](https://github.com/Earthcomputer/class-file-indexer/releases/latest) and install it manually using 22 | Settings/Preferences > Plugins > ⚙️ > Install plugin from disk... 23 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.changelog.markdownToHTML 2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 3 | import org.objectweb.asm.ClassReader 4 | import org.objectweb.asm.ClassWriter 5 | import org.objectweb.asm.commons.ClassRemapper 6 | import org.objectweb.asm.commons.Remapper 7 | import java.util.zip.ZipEntry 8 | import java.util.zip.ZipFile 9 | import java.util.zip.ZipOutputStream 10 | 11 | buildscript { 12 | repositories { 13 | mavenCentral() 14 | maven { 15 | url = uri("https://plugins.gradle.org/m2/") 16 | } 17 | } 18 | dependencies { 19 | classpath("org.ow2.asm:asm:9.3") 20 | classpath("org.ow2.asm:asm-commons:9.3") 21 | classpath("com.guardsquare:proguard-gradle:7.2.2") 22 | } 23 | } 24 | 25 | fun properties(key: String) = project.findProperty(key).toString() 26 | 27 | plugins { 28 | // Java support 29 | id("java") 30 | // Kotlin support 31 | id("org.jetbrains.kotlin.jvm") version "1.7.10" 32 | // Gradle IntelliJ Plugin 33 | id("org.jetbrains.intellij") version "1.8.0" 34 | // Gradle Changelog Plugin 35 | id("org.jetbrains.changelog") version "1.3.1" 36 | // Gradle Qodana Plugin 37 | // id("org.jetbrains.qodana") version "0.1.13" 38 | // ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle 39 | id("org.jlleitschuh.gradle.ktlint") version "10.3.0" 40 | } 41 | 42 | val artifactTypeAttribute = Attribute.of("artifactType", String::class.java) 43 | val repackagedAttribute = Attribute.of("repackaged", Boolean::class.javaObjectType) 44 | 45 | val repackage: Configuration by configurations.creating { 46 | attributes.attribute(repackagedAttribute, true) 47 | } 48 | 49 | group = properties("pluginGroup") 50 | version = properties("pluginVersion") 51 | 52 | fun getIDEAPath(): String { 53 | return properties("localIdeaPath") 54 | } 55 | 56 | // Configure project's dependencies 57 | abstract class MyRepackager : TransformAction { 58 | @InputArtifact 59 | abstract fun getInputArtifact(): Provider 60 | override fun transform(outputs: TransformOutputs) { 61 | val input = getInputArtifact().get().asFile 62 | val output = outputs.file( 63 | input.name.let { 64 | if (it.endsWith(".jar")) 65 | it.replaceRange(it.length - 4, it.length, "-repackaged.jar") 66 | else 67 | "$it-repackaged" 68 | } 69 | ) 70 | println("Repackaging ${input.absolutePath} to ${output.absolutePath}") 71 | ZipOutputStream(output.outputStream()).use { zipOut -> 72 | ZipFile(input).use { zipIn -> 73 | val entriesList = zipIn.entries().toList() 74 | val entriesSet = entriesList.mapTo(mutableSetOf()) { it.name } 75 | for (entry in entriesList) { 76 | val newName = if (entry.name.contains("/") && !entry.name.startsWith("META-INF/")) { 77 | "net/earthcomputer/classfileindexer/libs/" + entry.name 78 | } else { 79 | entry.name 80 | } 81 | zipOut.putNextEntry(ZipEntry(newName)) 82 | if (entry.name.endsWith(".class")) { 83 | val writer = ClassWriter(0) 84 | ClassReader(zipIn.getInputStream(entry)).accept( 85 | ClassRemapper( 86 | writer, 87 | object : Remapper() { 88 | override fun map(internalName: String?): String? { 89 | if (internalName == null) return null 90 | return if (entriesSet.contains("$internalName.class")) { 91 | "net/earthcomputer/classfileindexer/libs/$internalName" 92 | } else { 93 | internalName 94 | } 95 | } 96 | } 97 | ), 98 | 0 99 | ) 100 | zipOut.write( 101 | writer.toByteArray() 102 | ) 103 | } else { 104 | zipIn.getInputStream(entry).copyTo(zipOut) 105 | } 106 | zipOut.closeEntry() 107 | } 108 | } 109 | zipOut.flush() 110 | } 111 | } 112 | } 113 | 114 | repositories { 115 | mavenCentral() 116 | } 117 | dependencies { 118 | attributesSchema { 119 | attribute(repackagedAttribute) 120 | } 121 | artifactTypes.getByName("jar") { 122 | attributes.attribute(repackagedAttribute, false) 123 | } 124 | registerTransform(MyRepackager::class) { 125 | from.attribute(repackagedAttribute, false).attribute(artifactTypeAttribute, "jar") 126 | to.attribute(repackagedAttribute, true).attribute(artifactTypeAttribute, "jar") 127 | } 128 | 129 | repackage("org.ow2.asm:asm:9.3") 130 | implementation(files(repackage.files)) 131 | } 132 | 133 | // Configure gradle-intellij-plugin plugin. 134 | // Read more: https://github.com/JetBrains/gradle-intellij-plugin 135 | intellij { 136 | pluginName.set(properties("pluginName")) 137 | version.set(properties("platformVersion")) 138 | type.set(properties("platformType")) 139 | downloadSources.set(properties("platformDownloadSources").toBoolean()) 140 | updateSinceUntilBuild.set(true) 141 | 142 | // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. 143 | plugins.set(properties("platformPlugins").split(',').map(String::trim).filter(String::isNotEmpty)) 144 | } 145 | 146 | // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin 147 | changelog { 148 | version.set(properties("pluginVersion")) 149 | groups.set(emptyList()) 150 | } 151 | 152 | // Configure Gradle Qodana Plugin - read more: https://github.com/JetBrains/gradle-qodana-plugin 153 | /*qodana { 154 | cachePath.set(projectDir.resolve(".qodana").canonicalPath) 155 | reportPath.set(projectDir.resolve("build/reports/inspections").canonicalPath) 156 | saveReport.set(true) 157 | showReport.set(System.getenv("QODANA_SHOW_REPORT")?.toBoolean() ?: false) 158 | }*/ 159 | 160 | tasks.register("proguard") { 161 | verbose() 162 | 163 | // Alternatively put your config in a separate file 164 | // configuration("config.pro") 165 | 166 | // Use the jar task output as a input jar. This will automatically add the necessary task dependency. 167 | injars(tasks.named("jar")) 168 | 169 | outjars("build/${rootProject.name}-obfuscated.jar") 170 | 171 | val javaHome = System.getProperty("java.home") 172 | // Automatically handle the Java version of this build, don't support JBR 173 | // As of Java 9, the runtime classes are packaged in modular jmod files. 174 | // libraryjars( 175 | // // filters must be specified first, as a map 176 | // mapOf("jarfilter" to "!**.jar", 177 | // "filter" to "!module-info.class"), 178 | // "$javaHome/jmods/java.base.jmod" 179 | // ) 180 | 181 | print("javaHome=$javaHome") 182 | // Add all JDK deps 183 | if (!properties("skipProguard").toBoolean()) { 184 | File("$javaHome/jmods/") 185 | .listFiles()!! 186 | .forEach { 187 | libraryjars(it.absolutePath) 188 | } 189 | } 190 | 191 | // libraryjars(configurations.runtimeClasspath.get().files) 192 | val ideaPath = getIDEAPath() 193 | 194 | // Add all java plugins to classpath 195 | // File("$ideaPath/plugins/java/lib").listFiles()!!.forEach { libraryjars(it.absolutePath) } 196 | // Add all IDEA libs to classpath 197 | // File("$ideaPath/lib").listFiles()!!.forEach { libraryjars(it.absolutePath) } 198 | 199 | libraryjars(configurations.compileClasspath.get()) 200 | 201 | dontshrink() 202 | dontoptimize() 203 | 204 | // allowaccessmodification() //you probably shouldn't use this option when processing code that is to be used as a library, since classes and class members that weren't designed to be public in the API may become public 205 | 206 | adaptclassstrings("**.xml") 207 | adaptresourcefilecontents("**.xml") // or adaptresourcefilecontents() 208 | 209 | // Allow methods with the same signature, except for the return type, 210 | // to get the same obfuscation name. 211 | overloadaggressively() 212 | // Put all obfuscated classes into the nameless root package. 213 | // repackageclasses("") 214 | 215 | printmapping("build/proguard-mapping.txt") 216 | 217 | target("11") 218 | 219 | adaptresourcefilenames() 220 | optimizationpasses(9) 221 | allowaccessmodification() 222 | mergeinterfacesaggressively() 223 | renamesourcefileattribute("SourceFile") 224 | keepattributes("Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod") 225 | 226 | keep( 227 | """ class net.earthcomputer.classfileindexer.MyAgent{*;} 228 | """.trimIndent() 229 | ) 230 | keep( 231 | """ class net.earthcomputer.classfileindexer.MyAgent$*{*;} 232 | """.trimIndent() 233 | ) 234 | keep( 235 | """ class net.earthcomputer.classfileindexer.IHasCustomDescription{*;} 236 | """.trimIndent() 237 | ) 238 | keep( 239 | """ class net.earthcomputer.classfileindexer.IHasNavigationOffset{*;} 240 | """.trimIndent() 241 | ) 242 | keep( 243 | """ class net.earthcomputer.classfileindexer.IIsWriteOverride{*;} 244 | """.trimIndent() 245 | ) 246 | } 247 | 248 | tasks { 249 | // Set the JVM compatibility versions 250 | properties("javaVersion").let { 251 | withType { 252 | options.encoding = "UTF-8" 253 | sourceCompatibility = it 254 | targetCompatibility = it 255 | } 256 | withType { 257 | kotlinOptions.jvmTarget = it 258 | } 259 | } 260 | 261 | wrapper { 262 | gradleVersion = properties("gradleVersion") 263 | } 264 | 265 | patchPluginXml { 266 | version.set(properties("pluginVersion")) 267 | sinceBuild.set(properties("pluginSinceBuild")) 268 | untilBuild.set(properties("pluginUntilBuild")) 269 | 270 | // Extract the section from README.md and provide for the plugin's manifest 271 | pluginDescription.set( 272 | projectDir.resolve("README.md").readText().lines().run { 273 | val start = "" 274 | val end = "" 275 | 276 | if (!containsAll(listOf(start, end))) { 277 | throw GradleException("Plugin description section not found in README.md:\n$start ... $end") 278 | } 279 | subList(indexOf(start) + 1, indexOf(end)) 280 | }.joinToString("\n").run { markdownToHTML(this) } 281 | ) 282 | 283 | // Get the latest available change notes from the changelog file 284 | changeNotes.set( 285 | provider { 286 | changelog.run { 287 | getOrNull(properties("pluginVersion")) ?: getLatest() 288 | }.toHTML() 289 | } 290 | ) 291 | } 292 | 293 | prepareSandbox { 294 | if (!properties("skipProguard").toBoolean()) { 295 | dependsOn("proguard") 296 | doFirst { 297 | val original = File("build/libs/${rootProject.name}-${properties("pluginVersion")}.jar") 298 | println(original.absolutePath) 299 | val obfuscated = File("build/${rootProject.name}-obfuscated.jar") 300 | println(obfuscated.absolutePath) 301 | if (original.exists() && obfuscated.exists()) { 302 | original.delete() 303 | obfuscated.renameTo(original) 304 | println("plugin file obfuscated") 305 | } else { 306 | println("error: some file does not exist, plugin file not obfuscated") 307 | } 308 | } 309 | } 310 | } 311 | // Configure UI tests plugin 312 | // Read more: https://github.com/JetBrains/intellij-ui-test-robot 313 | runIdeForUiTests { 314 | systemProperty("robot-server.port", "8082") 315 | systemProperty("ide.mac.message.dialogs.as.sheets", "false") 316 | systemProperty("jb.privacy.policy.text", "") 317 | systemProperty("jb.consents.confirmation.enabled", "false") 318 | } 319 | 320 | signPlugin { 321 | certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) 322 | privateKey.set(System.getenv("PRIVATE_KEY")) 323 | password.set(System.getenv("PRIVATE_KEY_PASSWORD")) 324 | } 325 | 326 | publishPlugin { 327 | dependsOn("patchChangelog") 328 | token.set(System.getenv("PUBLISH_TOKEN")) 329 | // pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 330 | // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: 331 | // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel 332 | channels.set(listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first())) 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /detekt-config.yml: -------------------------------------------------------------------------------- 1 | # Default detekt configuration: 2 | # https://github.com/detekt/detekt/blob/master/detekt-core/src/main/resources/default-detekt-config.yml 3 | 4 | 5 | 6 | complexity: 7 | active: false 8 | 9 | exceptions: 10 | ThrowingExceptionsWithoutMessageOrCause: 11 | active: false 12 | 13 | formatting: 14 | Indentation: 15 | continuationIndentSize: 8 16 | MaximumLineLength: 17 | maxLineLength: 150 18 | 19 | performance: 20 | SpreadOperator: 21 | active: false 22 | 23 | style: 24 | ReturnCount: 25 | active: false 26 | MaxLineLength: 27 | maxLineLength: 150 28 | LoopWithTooManyJumpStatements: 29 | active: false 30 | ForbiddenComment: 31 | active: false 32 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories 2 | # -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 3 | 4 | pluginGroup = com.github.earthcomputer.classfileindexer 5 | pluginName = class-file-indexer 6 | pluginVersion = 1.1.3 7 | 8 | # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 9 | # for insight into build numbers and IntelliJ Platform versions. 10 | pluginSinceBuild = 212 11 | pluginUntilBuild = 222.* 12 | 13 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension 14 | #// Note that the default is 'LATEST-EAP-SNAPSHOT', but can be set to specific versions (e.g. '2020.1') 15 | platformType = IC 16 | platformVersion = 2022.1 17 | platformDownloadSources = true 18 | 19 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 20 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 21 | platformPlugins = java 22 | 23 | # Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3 24 | javaVersion = 11 25 | 26 | # Gradle Releases -> https://github.com/gradle/gradle/releases 27 | gradleVersion = 7.4.2 28 | 29 | # Opt-out flag for bundling Kotlin standard library -> https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library 30 | # suppress inspection "UnusedProperty" 31 | kotlin.stdlib.default.dependency = false 32 | 33 | skipProguard = false 34 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Earthcomputer/class-file-indexer/2fa48f049614acf71ad7746592f4b9d06c085ddc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /qodana.yml: -------------------------------------------------------------------------------- 1 | # Qodana configuration: 2 | # https://www.jetbrains.com/help/qodana/qodana-yaml.html 3 | 4 | version: 1.0 5 | profile: 6 | name: qodana.recommended 7 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "class-file-indexer" 2 | -------------------------------------------------------------------------------- /src/main/java/net/earthcomputer/classfileindexer/MyAgent.java: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer; 2 | 3 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.ClassReader; 4 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.ClassVisitor; 5 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.ClassWriter; 6 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.FieldVisitor; 7 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Label; 8 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.MethodVisitor; 9 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Opcodes; 10 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Type; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.lang.instrument.ClassFileTransformer; 15 | import java.lang.instrument.Instrumentation; 16 | import java.lang.instrument.UnmodifiableClassException; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.nio.file.Paths; 20 | import java.security.ProtectionDomain; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.Locale; 24 | import java.util.function.BiFunction; 25 | 26 | public class MyAgent implements ClassFileTransformer { 27 | private static final boolean DEBUG = false; 28 | 29 | private static final String USAGE_INFO = "com/intellij/usageView/UsageInfo"; 30 | private static final String USAGE_INFO_2_USAGE_ADAPTER = "com/intellij/usages/UsageInfo2UsageAdapter"; 31 | private static final String PSI_UTIL = "com/intellij/psi/util/PsiUtil"; 32 | private static final String JAVA_READ_WRITE_ACCESS_DETECTOR = "com/intellij/codeInsight/highlighting/JavaReadWriteAccessDetector"; 33 | 34 | public static void agentmain(String s, Instrumentation instrumentation) throws UnmodifiableClassException { 35 | instrumentation.addTransformer(new MyAgent(), true); 36 | 37 | List> classesToRetransform = new ArrayList<>(); 38 | for (Class clazz : instrumentation.getAllLoadedClasses()) { 39 | String className = clazz.getName(); 40 | if (USAGE_INFO.equals(className) 41 | || USAGE_INFO_2_USAGE_ADAPTER.equals(className) 42 | || PSI_UTIL.equals(className) 43 | || JAVA_READ_WRITE_ACCESS_DETECTOR.equals(className)) { 44 | classesToRetransform.add(clazz); 45 | } 46 | } 47 | instrumentation.retransformClasses(classesToRetransform.toArray(new Class[0])); 48 | } 49 | 50 | @Override 51 | public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { 52 | try { 53 | switch (className) { 54 | case USAGE_INFO: 55 | return transformClass( 56 | classfileBuffer, 57 | loader, 58 | "net.earthcomputer.classfileindexer.IHasNavigationOffset", 59 | "getNavigationOffset", 60 | "()I", 61 | new HookClassVisitor.Target( 62 | "getNavigationOffset", 63 | "()I", 64 | UsageInfoGetNavigationOffsetVisitor::new 65 | ) 66 | ); 67 | case USAGE_INFO_2_USAGE_ADAPTER: 68 | classfileBuffer = transformClass( 69 | classfileBuffer, 70 | loader, 71 | "net.earthcomputer.classfileindexer.IHasCustomDescription", 72 | "getCustomDescription", 73 | "()[Lcom/intellij/usages/TextChunk;", 74 | new HookClassVisitor.Target( 75 | "getPlainText", 76 | "()Ljava/lang/String;", 77 | HasCustomDescriptionVisitor::new 78 | ), 79 | new HookClassVisitor.Target( 80 | "initChunks", 81 | "()[Lcom/intellij/usages/TextChunk;", 82 | (methodVisitor, hookInfo) -> new ComputeTextMethodVisitor(methodVisitor, hookInfo, true) 83 | ), 84 | new HookClassVisitor.Target( 85 | "computeText", 86 | "()[Lcom/intellij/usages/TextChunk;", 87 | (methodVisitor1, hookInfo1) -> new ComputeTextMethodVisitor(methodVisitor1, hookInfo1, false) 88 | ) 89 | ); 90 | return transformClass( 91 | classfileBuffer, 92 | loader, 93 | "net.earthcomputer.classfileindexer.IHasNavigationOffset", 94 | "getLineNumber", 95 | "()I", 96 | new HookClassVisitor.Target( 97 | "", 98 | "(Lcom/intellij/usageView/UsageInfo;)V", 99 | UsageInfoLineNumberMethodVisitor::new 100 | ) 101 | ); 102 | case PSI_UTIL: 103 | return transformClass( 104 | classfileBuffer, 105 | loader, 106 | "net.earthcomputer.classfileindexer.IIsWriteOverride", 107 | "isWrite", 108 | "()Z", 109 | new HookClassVisitor.Target( 110 | "isAccessedForWriting", 111 | "(Lcom/intellij/psi/PsiExpression;)Z", 112 | IsAccessedForWriteMethodVisitor::new 113 | ) 114 | ); 115 | case JAVA_READ_WRITE_ACCESS_DETECTOR: 116 | return transformClass( 117 | classfileBuffer, 118 | loader, 119 | "net.earthcomputer.classfileindexer.IIsWriteOverride", 120 | "isWrite", 121 | "()Z", 122 | new HookClassVisitor.Target( 123 | "getExpressionAccess", 124 | "(Lcom/intellij/psi/PsiElement;)Lcom/intellij/codeInsight/highlighting/ReadWriteAccessDetector$Access;", 125 | JavaReadWriteAccessDetectorMethodVisitor::new 126 | ) 127 | ); 128 | } 129 | } catch (Throwable t) { 130 | // since the jdk does not log it for us 131 | t.printStackTrace(); 132 | throw t; 133 | } 134 | return classfileBuffer; 135 | } 136 | 137 | private static byte[] transformClass(byte[] classfileBuffer, ClassLoader classLoader, String interfaceName, String interfaceMethod, String interfaceMethodDesc, HookClassVisitor.Target... targets) { 138 | ClassReader cr = new ClassReader(classfileBuffer); 139 | String className = cr.getClassName(); 140 | ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES) { 141 | @Override 142 | protected String getCommonSuperClass(String type1, String type2) { 143 | if (type1.equals(type2)) { 144 | return type1; 145 | } 146 | if (type2.equals(className)) { 147 | return getCommonSuperClass(type2, type1); 148 | } 149 | if (type1.equals(className)) { 150 | String superName = cr.getSuperName(); 151 | return getCommonSuperClass(superName, type2); 152 | } 153 | return super.getCommonSuperClass(type1, type2); 154 | } 155 | 156 | @Override 157 | protected ClassLoader getClassLoader() { 158 | return classLoader; 159 | } 160 | }; 161 | cr.accept(new HookClassVisitor(cw, interfaceName, interfaceMethod, interfaceMethodDesc, targets), ClassReader.SKIP_FRAMES); 162 | byte[] bytes = cw.toByteArray(); 163 | if (DEBUG) { 164 | Path output = Paths.get("debugTransformerOutput", className.replace("/", File.separator) + ".class").toAbsolutePath(); 165 | try { 166 | if (!Files.exists(output.getParent())) { 167 | Files.createDirectories(output.getParent()); 168 | } 169 | Files.write(output, bytes); 170 | System.out.println("Written " + output); 171 | } catch (IOException e) { 172 | e.printStackTrace(); 173 | } 174 | } 175 | return bytes; 176 | } 177 | 178 | // expects an object to already be on the stack 179 | // injects code resembling the following: 180 | // if (obj instanceof interfaceName i) { // prologue 181 | // 182 | // result = i.interfaceMethod(); // interface call 183 | // 184 | // } // epilogue 185 | private static class HookClassVisitor extends ClassVisitor { 186 | static class Target { 187 | final String hookMethodName; 188 | final String hookMethodDesc; 189 | final BiFunction hookMethodTransformer; 190 | 191 | Target(String hookMethodName, String hookMethodDesc, BiFunction hookMethodTransformer) { 192 | this.hookMethodName = hookMethodName; 193 | this.hookMethodDesc = hookMethodDesc; 194 | this.hookMethodTransformer = hookMethodTransformer; 195 | } 196 | } 197 | static class HookInfo { 198 | final String interfaceName; 199 | final String interfaceMethod; 200 | final String interfaceMethodDesc; 201 | final Target[] targets; 202 | final String hookClassField; 203 | final String hookMethodField; 204 | String targetClass; 205 | 206 | HookInfo(String interfaceName, String interfaceMethod, String interfaceMethodDesc, Target... targets) { 207 | this.interfaceName = interfaceName; 208 | this.interfaceMethod = interfaceMethod; 209 | this.interfaceMethodDesc = interfaceMethodDesc; 210 | this.targets = targets; 211 | this.hookClassField = "C" + interfaceName.toUpperCase(Locale.ROOT).replace('.', '_'); 212 | this.hookMethodField = hookClassField + "_" + interfaceMethod.toUpperCase(Locale.ROOT); 213 | } 214 | 215 | } 216 | private final HookInfo hookInfo; 217 | private boolean hadClinit = false; 218 | 219 | public HookClassVisitor(ClassVisitor classVisitor, String interfaceName, String interfaceMethod, String interfaceMethodDesc, Target... targets) { 220 | super(Opcodes.ASM9, classVisitor); 221 | this.hookInfo = new HookInfo(interfaceName, interfaceMethod, interfaceMethodDesc, targets); 222 | } 223 | 224 | @Override 225 | public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { 226 | super.visit(version, access, name, signature, superName, interfaces); 227 | hookInfo.targetClass = name; 228 | } 229 | 230 | @Override 231 | public void visitEnd() { 232 | if (!hadClinit) { 233 | MethodVisitor mv = visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "", "()V", null, null); 234 | if (mv != null) { 235 | mv.visitCode(); 236 | mv.visitInsn(Opcodes.RETURN); 237 | mv.visitMaxs(0, 0); 238 | mv.visitEnd(); 239 | } 240 | } 241 | FieldVisitor fv = visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, hookInfo.hookClassField, "Ljava/lang/Class;", null, null); 242 | if (fv != null) 243 | fv.visitEnd(); 244 | fv = visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, hookInfo.hookMethodField, "Ljava/lang/reflect/Method;", null, null); 245 | if (fv != null) 246 | fv.visitEnd(); 247 | super.visitEnd(); 248 | } 249 | 250 | @Override 251 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { 252 | MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); 253 | if ("".equals(name)) { 254 | hadClinit = true; 255 | return new HookClinitVisitor(mv, hookInfo); 256 | } 257 | for (Target target : hookInfo.targets) { 258 | if (target.hookMethodName.equals(name) && target.hookMethodDesc.equals(descriptor)) { 259 | return target.hookMethodTransformer.apply(mv, hookInfo); 260 | } 261 | } 262 | return mv; 263 | } 264 | } 265 | 266 | private static class HookClinitVisitor extends MethodVisitor { 267 | private final HookClassVisitor.HookInfo hookInfo; 268 | 269 | public HookClinitVisitor(MethodVisitor methodVisitor, HookClassVisitor.HookInfo hookInfo) { 270 | super(Opcodes.ASM9, methodVisitor); 271 | this.hookInfo = hookInfo; 272 | } 273 | 274 | @Override 275 | public void visitCode() { 276 | super.visitCode(); 277 | visitMethodInsn(Opcodes.INVOKESTATIC, "com/intellij/ide/plugins/PluginManager", "getInstance", "()Lcom/intellij/ide/plugins/PluginManager;", false); 278 | visitLdcInsn("net.earthcomputer.classfileindexer"); 279 | visitMethodInsn(Opcodes.INVOKESTATIC, "com/intellij/openapi/extensions/PluginId", "getId", "(Ljava/lang/String;)Lcom/intellij/openapi/extensions/PluginId;", false); 280 | visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/intellij/ide/plugins/PluginManager", "findEnabledPlugin", "(Lcom/intellij/openapi/extensions/PluginId;)Lcom/intellij/ide/plugins/IdeaPluginDescriptor;", false); 281 | visitMethodInsn(Opcodes.INVOKEINTERFACE, "com/intellij/openapi/extensions/PluginDescriptor", "getPluginClassLoader", "()Ljava/lang/ClassLoader;", true); 282 | visitLdcInsn(hookInfo.interfaceName); 283 | visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/ClassLoader", "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", false); 284 | visitInsn(Opcodes.DUP); 285 | visitFieldInsn(Opcodes.PUTSTATIC, hookInfo.targetClass, hookInfo.hookClassField, "Ljava/lang/Class;"); 286 | visitLdcInsn(hookInfo.interfaceMethod); 287 | Type[] argTypes = Type.getMethodType(hookInfo.interfaceMethodDesc).getArgumentTypes(); 288 | loadInt(this, argTypes.length); 289 | visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Class"); 290 | for (int i = 0; i < argTypes.length; i++) { 291 | Type argType = argTypes[i]; 292 | visitInsn(Opcodes.DUP); 293 | loadInt(this, i); 294 | if (argType.getSort() == Type.OBJECT || argType.getSort() == Type.ARRAY) { 295 | visitLdcInsn(argType); 296 | } else { 297 | String boxedClass = getBoxedClass(argType); 298 | visitFieldInsn(Opcodes.GETSTATIC, boxedClass, "TYPE", "Ljava/lang/Class;"); 299 | } 300 | visitInsn(Opcodes.AASTORE); 301 | } 302 | visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getMethod", "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;", false); 303 | visitFieldInsn(Opcodes.PUTSTATIC, hookInfo.targetClass, hookInfo.hookMethodField, 304 | "Ljava/lang/reflect/Method;"); 305 | } 306 | 307 | } 308 | 309 | private static abstract class HookMethodVisitor extends MethodVisitor { 310 | private final HookClassVisitor.HookInfo hookInfo; 311 | private Label jumpLabel; 312 | 313 | public HookMethodVisitor(MethodVisitor methodVisitor, HookClassVisitor.HookInfo hookInfo) { 314 | super(Opcodes.ASM9, methodVisitor); 315 | this.hookInfo = hookInfo; 316 | } 317 | 318 | protected void addPrologue() { 319 | visitFieldInsn(Opcodes.GETSTATIC, hookInfo.targetClass, hookInfo.hookClassField, "Ljava/lang/Class;"); 320 | visitInsn(Opcodes.SWAP); 321 | visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "isInstance", "(Ljava/lang/Object;)Z", false); 322 | jumpLabel = new Label(); 323 | visitJumpInsn(Opcodes.IFEQ, jumpLabel); 324 | } 325 | 326 | protected void addInterfaceCall() { 327 | Type methodType = Type.getMethodType(hookInfo.interfaceMethodDesc); 328 | Type[] argTypes = methodType.getArgumentTypes(); 329 | loadInt(this, argTypes.length); 330 | visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object"); 331 | for (int i = argTypes.length - 1; i >= 0; i--) { 332 | Type argType = argTypes[i]; 333 | boolean isWide = argType.getSort() == Type.DOUBLE || argType.getSort() == Type.LONG; 334 | visitInsn(isWide ? Opcodes.DUP_X2 : Opcodes.DUP_X1); 335 | visitInsn(isWide ? Opcodes.DUP_X2 : Opcodes.DUP_X1); 336 | visitInsn(Opcodes.POP); 337 | if (argType.getSort() != Type.OBJECT && argType.getSort() != Type.ARRAY) { 338 | String boxedClass = getBoxedClass(argType); 339 | visitMethodInsn(Opcodes.INVOKESTATIC, boxedClass, "valueOf", "(" + argType.getDescriptor() + ")L" + boxedClass + ";", false); 340 | } 341 | loadInt(this, i); 342 | visitInsn(Opcodes.SWAP); 343 | visitInsn(Opcodes.AASTORE); 344 | } 345 | visitFieldInsn(Opcodes.GETSTATIC, hookInfo.targetClass, hookInfo.hookMethodField, "Ljava/lang/reflect/Method;"); 346 | visitInsn(Opcodes.DUP_X2); 347 | visitInsn(Opcodes.POP); 348 | visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/reflect/Method", "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", false); 349 | Type returnType = methodType.getReturnType(); 350 | if (!"Ljava/lang/Object;".equals(returnType.getDescriptor())) { 351 | if (returnType.getSort() == Type.VOID) { 352 | visitInsn(Opcodes.POP); 353 | } else if (returnType.getSort() == Type.OBJECT) { 354 | visitTypeInsn(Opcodes.CHECKCAST, returnType.getInternalName()); 355 | } else if (returnType.getSort() == Type.ARRAY) { 356 | visitTypeInsn(Opcodes.CHECKCAST, returnType.getDescriptor()); 357 | } else { 358 | String boxedClass = getBoxedClass(returnType); 359 | visitTypeInsn(Opcodes.CHECKCAST, boxedClass); 360 | String unboxMethod = getUnboxMethod(returnType); 361 | visitMethodInsn(Opcodes.INVOKEVIRTUAL, boxedClass, unboxMethod, "()" + returnType.getDescriptor(), false); 362 | } 363 | } 364 | } 365 | 366 | protected void addEpilogue() { 367 | visitLabel(jumpLabel); 368 | } 369 | } 370 | 371 | private static class UsageInfoGetNavigationOffsetVisitor extends HookMethodVisitor { 372 | private boolean waitingForAstore = false; 373 | private boolean waitingForElementNullCheck = false; 374 | private Label elementNullCheckJumpTarget = null; 375 | private int elementLocalVarIndex = -1; 376 | 377 | public UsageInfoGetNavigationOffsetVisitor(MethodVisitor methodVisitor, HookClassVisitor.HookInfo hookInfo) { 378 | super(methodVisitor, hookInfo); 379 | } 380 | 381 | @Override 382 | public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { 383 | if (USAGE_INFO.equals(owner) && "getElement".equals(name) && "()Lcom/intellij/psi/PsiElement;".equals(descriptor)) { 384 | waitingForAstore = true; 385 | } 386 | super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); 387 | } 388 | 389 | @Override 390 | public void visitVarInsn(int opcode, int var) { 391 | super.visitVarInsn(opcode, var); 392 | if (waitingForAstore) { 393 | waitingForAstore = false; 394 | elementLocalVarIndex = var; 395 | waitingForElementNullCheck = true; 396 | } 397 | } 398 | 399 | @Override 400 | public void visitJumpInsn(int opcode, Label label) { 401 | super.visitJumpInsn(opcode, label); 402 | if (waitingForElementNullCheck) { 403 | waitingForElementNullCheck = false; 404 | elementNullCheckJumpTarget = label; 405 | } 406 | } 407 | 408 | @Override 409 | public void visitLabel(Label label) { 410 | super.visitLabel(label); 411 | if (elementNullCheckJumpTarget == label) { 412 | elementNullCheckJumpTarget = null; 413 | visitVarInsn(Opcodes.ALOAD, elementLocalVarIndex); 414 | addPrologue(); 415 | visitVarInsn(Opcodes.ALOAD, elementLocalVarIndex); 416 | addInterfaceCall(); 417 | visitInsn(Opcodes.IRETURN); 418 | addEpilogue(); 419 | } 420 | } 421 | } 422 | 423 | private static class HasCustomDescriptionVisitor extends HookMethodVisitor { 424 | private boolean waitingForAstore = false; 425 | 426 | public HasCustomDescriptionVisitor(MethodVisitor methodVisitor, HookClassVisitor.HookInfo hookInfo) { 427 | super(methodVisitor, hookInfo); 428 | } 429 | 430 | @Override 431 | public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { 432 | super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); 433 | if (USAGE_INFO_2_USAGE_ADAPTER.equals(owner) && "getElement".equals(name) && "()Lcom/intellij/psi/PsiElement;".equals(descriptor)) { 434 | waitingForAstore = true; 435 | } 436 | } 437 | 438 | @Override 439 | public void visitVarInsn(int opcode, int var) { 440 | super.visitVarInsn(opcode, var); 441 | if (waitingForAstore) { 442 | waitingForAstore = false; 443 | visitVarInsn(Opcodes.ALOAD, var); 444 | addPrologue(); 445 | visitVarInsn(Opcodes.ALOAD, var); 446 | addInterfaceCall(); 447 | visitVarInsn(Opcodes.ASTORE, var); 448 | visitVarInsn(Opcodes.ALOAD, var); 449 | visitInsn(Opcodes.ARRAYLENGTH); 450 | visitVarInsn(Opcodes.ISTORE, var + 1); 451 | visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder"); 452 | visitInsn(Opcodes.DUP); 453 | visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false); 454 | visitVarInsn(Opcodes.ASTORE, var + 2); 455 | visitInsn(Opcodes.ICONST_0); 456 | visitVarInsn(Opcodes.ISTORE, var + 3); 457 | Label loopCondition = new Label(); 458 | visitJumpInsn(Opcodes.GOTO, loopCondition); 459 | Label loopBody = new Label(); 460 | visitLabel(loopBody); 461 | visitVarInsn(Opcodes.ALOAD, var + 2); 462 | visitVarInsn(Opcodes.ALOAD, var); 463 | visitVarInsn(Opcodes.ILOAD, var + 3); 464 | visitInsn(Opcodes.AALOAD); 465 | visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/intellij/usages/TextChunk", "getText", "()Ljava/lang/String;", false); 466 | visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); 467 | visitInsn(Opcodes.POP); 468 | visitIincInsn(var + 3, 1); 469 | visitLabel(loopCondition); 470 | visitVarInsn(Opcodes.ILOAD, var + 3); 471 | visitVarInsn(Opcodes.ILOAD, var + 1); 472 | visitJumpInsn(Opcodes.IF_ICMPLT, loopBody); 473 | visitVarInsn(Opcodes.ALOAD, var + 2); 474 | visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false); 475 | visitInsn(Opcodes.ARETURN); 476 | addEpilogue(); 477 | } 478 | } 479 | } 480 | 481 | private static class ComputeTextMethodVisitor extends HookMethodVisitor { 482 | private final boolean storeToCacheField; 483 | private boolean waitingForAstore = false; 484 | 485 | public ComputeTextMethodVisitor(MethodVisitor methodVisitor, HookClassVisitor.HookInfo hookInfo, boolean storeToCacheField) { 486 | super(methodVisitor, hookInfo); 487 | this.storeToCacheField = storeToCacheField; 488 | } 489 | 490 | @Override 491 | public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { 492 | super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); 493 | if (USAGE_INFO_2_USAGE_ADAPTER.equals(owner) && "getElement".equals(name) && "()Lcom/intellij/psi/PsiElement;".equals(descriptor)) { 494 | waitingForAstore = true; 495 | } 496 | } 497 | 498 | @Override 499 | public void visitVarInsn(int opcode, int var) { 500 | super.visitVarInsn(opcode, var); 501 | if (waitingForAstore) { 502 | waitingForAstore = false; 503 | visitVarInsn(Opcodes.ALOAD, var); 504 | addPrologue(); 505 | visitVarInsn(Opcodes.ALOAD, var); 506 | addInterfaceCall(); 507 | visitVarInsn(Opcodes.ASTORE, var); 508 | if (storeToCacheField) { 509 | visitVarInsn(Opcodes.ALOAD, 0); 510 | visitTypeInsn(Opcodes.NEW, "com/intellij/reference/SoftReference"); 511 | visitInsn(Opcodes.DUP); 512 | visitVarInsn(Opcodes.ALOAD, var); 513 | visitMethodInsn(Opcodes.INVOKESPECIAL, "com/intellij/reference/SoftReference", "", "(Ljava/lang/Object;)V", false); 514 | visitFieldInsn(Opcodes.PUTFIELD, USAGE_INFO_2_USAGE_ADAPTER, "myTextChunks", "Ljava/lang/ref/Reference;"); 515 | } 516 | visitVarInsn(Opcodes.ALOAD, var); 517 | visitInsn(Opcodes.ARETURN); 518 | addEpilogue(); 519 | } 520 | } 521 | } 522 | 523 | private static class IsAccessedForWriteMethodVisitor extends HookMethodVisitor { 524 | public IsAccessedForWriteMethodVisitor(MethodVisitor methodVisitor, HookClassVisitor.HookInfo hookInfo) { 525 | super(methodVisitor, hookInfo); 526 | } 527 | 528 | @Override 529 | public void visitCode() { 530 | super.visitCode(); 531 | visitVarInsn(Opcodes.ALOAD, 0); 532 | addPrologue(); 533 | visitVarInsn(Opcodes.ALOAD, 0); 534 | addInterfaceCall(); 535 | visitInsn(Opcodes.IRETURN); 536 | addEpilogue(); 537 | } 538 | } 539 | 540 | private static class JavaReadWriteAccessDetectorMethodVisitor extends HookMethodVisitor { 541 | public JavaReadWriteAccessDetectorMethodVisitor(MethodVisitor methodVisitor, HookClassVisitor.HookInfo hookInfo) { 542 | super(methodVisitor, hookInfo); 543 | } 544 | 545 | @Override 546 | public void visitCode() { 547 | super.visitCode(); 548 | visitVarInsn(Opcodes.ALOAD, 1); 549 | addPrologue(); 550 | visitVarInsn(Opcodes.ALOAD, 1); 551 | addInterfaceCall(); 552 | Label falseLabel = new Label(); 553 | Label returnLabel = new Label(); 554 | visitJumpInsn(Opcodes.IFEQ, falseLabel); 555 | visitFieldInsn(Opcodes.GETSTATIC, "com/intellij/codeInsight/highlighting/ReadWriteAccessDetector$Access", "Write", "Lcom/intellij/codeInsight/highlighting/ReadWriteAccessDetector$Access;"); 556 | visitJumpInsn(Opcodes.GOTO, returnLabel); 557 | visitLabel(falseLabel); 558 | visitFieldInsn(Opcodes.GETSTATIC, "com/intellij/codeInsight/highlighting/ReadWriteAccessDetector$Access", "Read", "Lcom/intellij/codeInsight/highlighting/ReadWriteAccessDetector$Access;"); 559 | visitLabel(returnLabel); 560 | visitInsn(Opcodes.ARETURN); 561 | addEpilogue(); 562 | } 563 | } 564 | 565 | private static class UsageInfoLineNumberMethodVisitor extends HookMethodVisitor { 566 | public UsageInfoLineNumberMethodVisitor(MethodVisitor methodVisitor, HookClassVisitor.HookInfo hookInfo) { 567 | super(methodVisitor, hookInfo); 568 | } 569 | 570 | @Override 571 | public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { 572 | if (opcode == Opcodes.PUTFIELD && owner.equals(USAGE_INFO_2_USAGE_ADAPTER) && name.equals("myLineNumber")) { 573 | visitVarInsn(Opcodes.ALOAD, 0); 574 | visitMethodInsn(Opcodes.INVOKEVIRTUAL, USAGE_INFO_2_USAGE_ADAPTER, "getElement", "()Lcom/intellij/psi/PsiElement;", false); 575 | addPrologue(); 576 | visitInsn(Opcodes.POP); 577 | visitVarInsn(Opcodes.ALOAD, 0); 578 | visitMethodInsn(Opcodes.INVOKEVIRTUAL, USAGE_INFO_2_USAGE_ADAPTER, "getElement", "()Lcom/intellij/psi/PsiElement;", false); 579 | addInterfaceCall(); 580 | addEpilogue(); 581 | } 582 | super.visitFieldInsn(opcode, owner, name, descriptor); 583 | } 584 | } 585 | 586 | private static void loadInt(MethodVisitor mv, int val) { 587 | assert val >= 0; 588 | if (val <= 5) { 589 | mv.visitInsn(Opcodes.ICONST_0 + val); 590 | } else if (val <= 255) { 591 | mv.visitIntInsn(val <= 127 ? Opcodes.BIPUSH : Opcodes.SIPUSH, val); 592 | } else { 593 | mv.visitLdcInsn(val); 594 | } 595 | } 596 | 597 | private static String getBoxedClass(Type type) { 598 | switch (type.getSort()) { 599 | case Type.BYTE: 600 | return "java/lang/Byte"; 601 | case Type.CHAR: 602 | return "java/lang/Character"; 603 | case Type.DOUBLE: 604 | return "java/lang/Double"; 605 | case Type.FLOAT: 606 | return "java/lang/Float"; 607 | case Type.INT: 608 | return "java/lang/Integer"; 609 | case Type.LONG: 610 | return "java/lang/Long"; 611 | case Type.SHORT: 612 | return "java/lang/Short"; 613 | case Type.BOOLEAN: 614 | return "java/lang/Boolean"; 615 | default: 616 | throw new AssertionError(); 617 | } 618 | } 619 | 620 | private static String getUnboxMethod(Type type) { 621 | switch (type.getSort()) { 622 | case Type.BYTE: 623 | return "byteValue"; 624 | case Type.CHAR: 625 | return "charValue"; 626 | case Type.DOUBLE: 627 | return "doubleValue"; 628 | case Type.FLOAT: 629 | return "floatValue"; 630 | case Type.INT: 631 | return "intValue"; 632 | case Type.LONG: 633 | return "longValue"; 634 | case Type.SHORT: 635 | return "shortValue"; 636 | case Type.BOOLEAN: 637 | return "booleanValue"; 638 | default: 639 | throw new AssertionError(); 640 | } 641 | } 642 | } 643 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/AgentInitializedListener.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.ide.ApplicationInitializedListener 4 | import com.intellij.ide.plugins.PluginManager 5 | import com.intellij.openapi.extensions.PluginId 6 | import com.intellij.util.io.isFile 7 | import net.bytebuddy.agent.ByteBuddyAgent 8 | import java.io.File 9 | import java.io.InputStream 10 | import java.lang.management.ManagementFactory 11 | import java.nio.file.Files 12 | import java.util.jar.Attributes 13 | import java.util.jar.JarEntry 14 | import java.util.jar.JarFile 15 | import java.util.jar.JarOutputStream 16 | import java.util.jar.Manifest 17 | 18 | @Suppress("UnstableApiUsage") // there's no other way 19 | class AgentInitializedListener : ApplicationInitializedListener { 20 | companion object { 21 | const val AGENT_CLASS_NAME = "net.earthcomputer.classfileindexer.MyAgent" 22 | } 23 | 24 | override fun componentsInitialized() { 25 | val jarFile = File.createTempFile("agent", ".jar") 26 | val jarPath = jarFile.toPath() 27 | 28 | val manifest = Manifest() 29 | manifest.mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0" 30 | manifest.mainAttributes[Attributes.Name("Agent-Class")] = AGENT_CLASS_NAME 31 | manifest.mainAttributes[Attributes.Name("Can-Retransform-Classes")] = "true" 32 | manifest.mainAttributes[Attributes.Name("Can-Redefine-Classes")] = "true" 33 | 34 | JarOutputStream(Files.newOutputStream(jarPath), manifest).use { jar -> 35 | fun copyAgentClass(agentClassName: String) { 36 | val entryName = agentClassName.replace('.', '/') + ".class" 37 | jar.putNextEntry(JarEntry(entryName)) 38 | 39 | val (input, closeable) = findAgentClass(agentClassName) ?: throw AssertionError() 40 | if (closeable != null) { 41 | closeable.use { 42 | input.use { 43 | input.copyTo(jar) 44 | } 45 | } 46 | } else { 47 | input.use { 48 | input.copyTo(jar) 49 | } 50 | } 51 | 52 | jar.closeEntry() 53 | } 54 | 55 | fun writeEntry(name: String, inputStream: InputStream) { 56 | jar.putNextEntry(JarEntry(name)) 57 | inputStream.copyTo(jar) 58 | jar.closeEntry() 59 | } 60 | 61 | copyAgentClass(AGENT_CLASS_NAME) 62 | copyAgentClass("$AGENT_CLASS_NAME\$1") 63 | copyAgentClass("$AGENT_CLASS_NAME\$HookClassVisitor") 64 | copyAgentClass("$AGENT_CLASS_NAME\$HookClassVisitor\$Target") 65 | copyAgentClass("$AGENT_CLASS_NAME\$HookClassVisitor\$HookInfo") 66 | copyAgentClass("$AGENT_CLASS_NAME\$HookClinitVisitor") 67 | copyAgentClass("$AGENT_CLASS_NAME\$HookMethodVisitor") 68 | copyAgentClass("$AGENT_CLASS_NAME\$UsageInfoGetNavigationOffsetVisitor") 69 | copyAgentClass("$AGENT_CLASS_NAME\$HasCustomDescriptionVisitor") 70 | copyAgentClass("$AGENT_CLASS_NAME\$ComputeTextMethodVisitor") 71 | copyAgentClass("$AGENT_CLASS_NAME\$IsAccessedForWriteMethodVisitor") 72 | copyAgentClass("$AGENT_CLASS_NAME\$JavaReadWriteAccessDetectorMethodVisitor") 73 | copyAgentClass("$AGENT_CLASS_NAME\$UsageInfoLineNumberMethodVisitor") 74 | 75 | copyAllAgentClasses("net.earthcomputer.classfileindexer.libs.org.objectweb.asm.", ::writeEntry) 76 | } 77 | 78 | val runtimeMxBeanName = ManagementFactory.getRuntimeMXBean().name 79 | val pid = runtimeMxBeanName.substringBefore('@') 80 | 81 | ByteBuddyAgent.attach(jarFile, pid) 82 | 83 | jarFile.deleteOnExit() 84 | } 85 | 86 | private fun findAgentClass(agentClassName: String): Pair? { 87 | val pluginId = PluginId.findId("net.earthcomputer.classfileindexer") ?: return null 88 | val pluginPath = PluginManager.getInstance().findEnabledPlugin(pluginId)?.pluginPath ?: return null 89 | val entryName = agentClassName.replace('.', '/') + ".class" 90 | 91 | if (!Files.isDirectory(pluginPath)) { 92 | val pluginJar = JarFile(pluginPath.toFile()) 93 | val stream = pluginJar.getInputStream(pluginJar.getJarEntry(entryName)) 94 | return Pair(stream, pluginJar) 95 | } 96 | 97 | val relPath = agentClassName.replace(".", File.separator) + ".class" 98 | var path = pluginPath.resolve(relPath) 99 | if (Files.exists(path)) { 100 | return Pair(Files.newInputStream(path), null) 101 | } 102 | path = pluginPath.resolve("classes").resolve(relPath) 103 | if (Files.exists(path)) { 104 | return Pair(Files.newInputStream(path), null) 105 | } 106 | 107 | path = pluginPath.resolve("lib") 108 | if (Files.exists(path)) { 109 | for (file in path.toFile().listFiles() ?: return null) { 110 | if (!file.name.endsWith(".jar")) { 111 | continue 112 | } 113 | val jarPath = file.toPath() 114 | val jar = JarFile(jarPath.toFile()) 115 | val jarEntry = jar.getJarEntry(entryName) 116 | if (jarEntry == null) { 117 | jar.close() 118 | } else { 119 | return Pair(jar.getInputStream(jarEntry), jar) 120 | } 121 | } 122 | } 123 | 124 | return null 125 | } 126 | 127 | private fun copyAllAgentClasses(prefix: String, consumer: (String, InputStream) -> Unit) { 128 | val pluginId = PluginId.findId("net.earthcomputer.classfileindexer") ?: return 129 | val pluginPath = PluginManager.getInstance().findEnabledPlugin(pluginId)?.pluginPath ?: return 130 | val entryPrefix = prefix.replace('.', '/') 131 | 132 | if (!Files.isDirectory(pluginPath)) { 133 | JarFile(pluginPath.toFile()).use { pluginJar -> 134 | val entries = pluginJar.entries() 135 | while (entries.hasMoreElements()) { 136 | val entry = entries.nextElement() 137 | if (entry.name.startsWith(entryPrefix)) { 138 | consumer(entry.name, pluginJar.getInputStream(entry)) 139 | } 140 | } 141 | } 142 | } 143 | 144 | val relPath = prefix.replace(".", File.separator) 145 | var path = pluginPath.resolve(relPath) 146 | if (Files.exists(path)) { 147 | Files.walk(path).filter { it.isFile() }.forEach { file -> 148 | consumer(pluginPath.relativize(file).toString().replace(File.separator, "/"), Files.newInputStream(file)) 149 | } 150 | } 151 | val basePath = pluginPath.resolve("classes") 152 | path = basePath.resolve(relPath) 153 | if (Files.exists(path)) { 154 | Files.walk(path).filter { it.isFile() }.forEach { file -> 155 | consumer(basePath.relativize(file).toString().replace(File.separator, "/"), Files.newInputStream(file)) 156 | } 157 | } 158 | 159 | path = pluginPath.resolve("lib") 160 | if (Files.exists(path)) { 161 | for (file in path.toFile().listFiles() ?: return) { 162 | if (!file.name.endsWith(".jar")) { 163 | continue 164 | } 165 | val jarPath = file.toPath() 166 | JarFile(jarPath.toFile()).use { jar -> 167 | val entries = jar.entries() 168 | while (entries.hasMoreElements()) { 169 | val entry = entries.nextElement() 170 | if (entry.name.startsWith(entryPrefix)) { 171 | consumer(entry.name, jar.getInputStream(entry)) 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/BinaryIndexKey.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.util.io.DataInputOutputUtil 4 | import java.io.DataInput 5 | import java.io.DataOutput 6 | import java.io.IOException 7 | 8 | sealed class BinaryIndexKey(private val id: Int) { 9 | override fun hashCode() = id 10 | override fun equals(other: Any?) = id == (other as? BinaryIndexKey)?.id 11 | override fun toString() = "${javaClass.simpleName}.INSTANCE" 12 | 13 | open fun write(output: DataOutput, writeString: (DataOutput, String) -> Unit) { 14 | DataInputOutputUtil.writeINT(output, id) 15 | } 16 | companion object { 17 | fun read(input: DataInput, readString: (DataInput) -> String): BinaryIndexKey { 18 | return when (DataInputOutputUtil.readINT(input)) { 19 | ClassIndexKey.ID -> ClassIndexKey.INSTANCE 20 | FieldIndexKey.ID -> FieldIndexKey.read(input, readString) 21 | MethodIndexKey.ID -> MethodIndexKey.read(input, readString) 22 | StringConstantKey.ID -> StringConstantKey.INSTANCE 23 | ImplicitToStringKey.ID -> ImplicitToStringKey.INSTANCE 24 | DelegateIndexKey.ID -> DelegateIndexKey.read(input, readString) 25 | else -> throw IOException("Unknown binary index key type") 26 | } 27 | } 28 | } 29 | } 30 | class ClassIndexKey private constructor() : BinaryIndexKey(ID) { 31 | companion object { 32 | const val ID = 0 33 | val INSTANCE = ClassIndexKey() 34 | } 35 | } 36 | class FieldIndexKey(val owner: String, val isWrite: Boolean) : BinaryIndexKey(ID) { 37 | override fun hashCode() = 31 * (31 * owner.hashCode() + isWrite.hashCode()) + super.hashCode() 38 | override fun equals(other: Any?): Boolean { 39 | if (!super.equals(other)) return false 40 | val that = other as FieldIndexKey 41 | return owner == that.owner && isWrite == that.isWrite 42 | } 43 | override fun toString() = "FieldIndexKey($owner, $isWrite)" 44 | 45 | override fun write(output: DataOutput, writeString: (DataOutput, String) -> Unit) { 46 | super.write(output, writeString) 47 | writeString(output, owner) 48 | output.writeBoolean(isWrite) 49 | } 50 | 51 | companion object { 52 | const val ID = 1 53 | fun read(input: DataInput, readString: (DataInput) -> String) = FieldIndexKey(readString(input), input.readBoolean()) 54 | } 55 | } 56 | class MethodIndexKey(val owner: String, val desc: String) : BinaryIndexKey(ID) { 57 | override fun hashCode() = 31 * (31 * owner.hashCode() + desc.hashCode()) + super.hashCode() 58 | override fun equals(other: Any?): Boolean { 59 | if (!super.equals(other)) return false 60 | val that = other as MethodIndexKey 61 | return owner == that.owner && desc == that.desc 62 | } 63 | override fun toString() = "MethodIndexKey($owner, $desc)" 64 | 65 | override fun write(output: DataOutput, writeString: (DataOutput, String) -> Unit) { 66 | super.write(output, writeString) 67 | writeString(output, owner) 68 | writeString(output, desc) 69 | } 70 | 71 | companion object { 72 | const val ID = 2 73 | fun read(input: DataInput, readString: (DataInput) -> String) = MethodIndexKey(readString(input), readString(input)) 74 | } 75 | } 76 | class StringConstantKey private constructor() : BinaryIndexKey(ID) { 77 | companion object { 78 | const val ID = 3 79 | val INSTANCE = StringConstantKey() 80 | } 81 | } 82 | class ImplicitToStringKey private constructor() : BinaryIndexKey(ID) { 83 | companion object { 84 | const val ID = 4 85 | val INSTANCE = ImplicitToStringKey() 86 | } 87 | } 88 | class DelegateIndexKey(val key: BinaryIndexKey) : BinaryIndexKey(ID) { 89 | override fun hashCode() = 31 * key.hashCode() + super.hashCode() 90 | override fun equals(other: Any?): Boolean { 91 | if (!super.equals(other)) return false 92 | val that = other as DelegateIndexKey 93 | return key == that.key 94 | } 95 | override fun toString() = "DelegateIndexKey($key)" 96 | 97 | override fun write(output: DataOutput, writeString: (DataOutput, String) -> Unit) { 98 | super.write(output, writeString) 99 | key.write(output, writeString) 100 | } 101 | 102 | companion object { 103 | const val ID = 5 104 | fun read(input: DataInput, readString: (DataInput) -> String) = DelegateIndexKey(BinaryIndexKey.read(input, readString)) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/ClassFileIndex.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.openapi.progress.ProgressManager 4 | import com.intellij.openapi.util.RecursionManager 5 | import com.intellij.openapi.vfs.VirtualFile 6 | import com.intellij.psi.search.GlobalSearchScope 7 | import com.intellij.psi.search.SearchScope 8 | import com.intellij.util.indexing.FileBasedIndex 9 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.ClassReader 10 | 11 | object ClassFileIndex { 12 | fun search(name: String, key: BinaryIndexKey, scope: SearchScope): Map> { 13 | val globalScope = asGlobal(scope) 14 | val files = mutableMapOf>() 15 | val locationsToSearchFurther = mutableSetOf>() 16 | FileBasedIndex.getInstance().processValues( 17 | ClassFileIndexExtension.INDEX_ID, name, null, 18 | { file, value -> 19 | ProgressManager.checkCanceled() 20 | val className by lazy { 21 | file.inputStream.use { 22 | ClassReader(it).className 23 | } 24 | } 25 | value[key]?.let { 26 | files[file] = it.toMutableMap() 27 | } 28 | value[DelegateIndexKey(key)]?.let { delegate -> 29 | delegate.keys.mapTo(locationsToSearchFurther) { Pair(it, className) } 30 | } 31 | true 32 | }, 33 | globalScope 34 | ) 35 | for ((location, owner) in locationsToSearchFurther) { 36 | searchLocation(location, owner, globalScope) { file, sourceMap -> 37 | val targetMap = files.computeIfAbsent(file) { mutableMapOf() } 38 | for ((k, v) in sourceMap) { 39 | targetMap.merge(k, v, Integer::sum) 40 | } 41 | } 42 | } 43 | return files 44 | } 45 | 46 | fun search( 47 | name: String, 48 | keyPredicate: (BinaryIndexKey) -> Boolean, 49 | scope: SearchScope 50 | ): Map> { 51 | val result = mutableMapOf>() 52 | for ((file, keys) in searchReturnKeys(name, keyPredicate, scope)) { 53 | val targetMap = mutableMapOf() 54 | for (value in keys.values) { 55 | for ((k, v) in value) { 56 | targetMap.merge(k, v, Integer::sum) 57 | } 58 | } 59 | result[file] = targetMap 60 | } 61 | return result 62 | } 63 | 64 | fun searchReturnKeys( 65 | name: String, 66 | keyPredicate: (BinaryIndexKey) -> Boolean, 67 | scope: SearchScope 68 | ): Map>> { 69 | val globalScope = asGlobal(scope) 70 | val files = mutableMapOf>>() 71 | val locationsToSearchFurther = mutableSetOf>() 72 | FileBasedIndex.getInstance().processValues( 73 | ClassFileIndexExtension.INDEX_ID, name, null, 74 | { file, value -> 75 | ProgressManager.checkCanceled() 76 | val className by lazy { 77 | file.inputStream.use { 78 | ClassReader(it).className 79 | } 80 | } 81 | for ((key, v) in value) { 82 | if (keyPredicate(key)) { 83 | files.computeIfAbsent(file) { mutableMapOf() }[key] = v.toMutableMap() 84 | } else if (key is DelegateIndexKey && keyPredicate(key.key)) { 85 | v.keys.mapTo(locationsToSearchFurther) { Triple(key.key, it, className) } 86 | } 87 | } 88 | true 89 | }, 90 | globalScope 91 | ) 92 | for ((key, location, owner) in locationsToSearchFurther) { 93 | searchLocation(location, owner, globalScope) { file, sourceMap -> 94 | val targetMap = files.computeIfAbsent(file) { mutableMapOf() } 95 | .computeIfAbsent(key) { mutableMapOf() } 96 | for ((k, v1) in sourceMap) { 97 | targetMap.merge(k, v1, Integer::sum) 98 | } 99 | } 100 | } 101 | return files 102 | } 103 | 104 | private fun searchLocation( 105 | location: String, 106 | owner: String, 107 | scope: GlobalSearchScope, 108 | consumer: (VirtualFile, Map) -> Unit 109 | ) { 110 | RecursionManager.doPreventingRecursion(Pair(location, owner), true) { 111 | val name = location.substringBefore(":") 112 | val desc = location.substringAfter(":") 113 | if (desc.contains("(")) { 114 | search(name, MethodIndexKey(owner, desc), scope).forEach(consumer) 115 | } else { 116 | search(name, FieldIndexKey(owner, false), scope).forEach(consumer) 117 | search(name, FieldIndexKey(owner, true), scope).forEach(consumer) 118 | } 119 | } 120 | } 121 | 122 | private fun asGlobal(scope: SearchScope) = scope as? GlobalSearchScope ?: GlobalSearchScope.EMPTY_SCOPE.union(scope) 123 | } 124 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/ClassFileIndexExtension.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.ide.highlighter.JavaClassFileType 4 | import com.intellij.openapi.progress.ProgressManager 5 | import com.intellij.util.indexing.DataIndexer 6 | import com.intellij.util.indexing.DefaultFileTypeSpecificInputFilter 7 | import com.intellij.util.indexing.FileBasedIndexExtension 8 | import com.intellij.util.indexing.FileContent 9 | import com.intellij.util.indexing.ID 10 | import com.intellij.util.io.DataExternalizer 11 | import com.intellij.util.io.DataInputOutputUtil 12 | import com.intellij.util.io.KeyDescriptor 13 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.ClassReader 14 | import java.io.DataInput 15 | import java.io.DataOutput 16 | 17 | class ClassFileIndexExtension : 18 | FileBasedIndexExtension>>() { 19 | override fun getName() = INDEX_ID 20 | 21 | override fun getIndexer() = DataIndexer>, FileContent> { content -> 22 | val bytes = content.content 23 | val cv = IndexerClassVisitor() 24 | ClassReader(bytes).accept(cv, ClassReader.SKIP_FRAMES) 25 | @Suppress("USELESS_CAST") // kotlin compiler bug 26 | cv.index as Map>> 27 | } 28 | 29 | override fun getKeyDescriptor() = object : KeyDescriptor { 30 | override fun getHashCode(value: String): Int = value.hashCode() 31 | 32 | override fun isEqual(val1: String, val2: String) = val1 == val2 33 | 34 | override fun save(out: DataOutput, value: String) { 35 | writeString(out, value) 36 | } 37 | 38 | override fun read(input: DataInput) = readString(input) 39 | } 40 | 41 | override fun getValueExternalizer() = object : DataExternalizer>> { 42 | override fun save(out: DataOutput, value: Map>) { 43 | ProgressManager.checkCanceled() 44 | DataInputOutputUtil.writeINT(out, value.size) 45 | for ((key, counts) in value) { 46 | key.write(out, ::writeString) 47 | DataInputOutputUtil.writeINT(out, counts.size) 48 | for ((location, count) in counts) { 49 | writeString(out, location) 50 | DataInputOutputUtil.writeINT(out, count) 51 | } 52 | } 53 | } 54 | 55 | override fun read(input: DataInput): Map> { 56 | ProgressManager.checkCanceled() 57 | val result = SmartMap>() 58 | repeat(DataInputOutputUtil.readINT(input)) { 59 | val key = BinaryIndexKey.read(input, ::readString) 60 | val counts = SmartMap() 61 | repeat(DataInputOutputUtil.readINT(input)) { 62 | val location = readString(input) 63 | val count = DataInputOutputUtil.readINT(input) 64 | counts[location] = count 65 | } 66 | result[key] = counts 67 | } 68 | return result 69 | } 70 | } 71 | 72 | override fun getVersion() = 4 73 | 74 | override fun getInputFilter() = DefaultFileTypeSpecificInputFilter(JavaClassFileType.INSTANCE) 75 | 76 | override fun dependsOnFileContent() = true 77 | 78 | companion object { 79 | // private val LOGGER = Logger.getInstance(ClassFileIndexExtension::class.java) 80 | val INDEX_ID = ID.create>>("classfileindexer.index") 81 | // private const val ENUMERATOR_INITIAL_SIZE = 1024 * 4 82 | } 83 | 84 | // TODO: when all this becomes stable API... 85 | // TODO: could also take advantage of custom map implementations to call ProgressManager.checkCanceled() more often 86 | // private val enumeratorPath: Path = IndexInfrastructure.getIndexRootDir(INDEX_ID).resolve("classfileindexer.constpool") 87 | // private var enumerator = createEnumerator() 88 | // 89 | // private fun createEnumerator(): PersistentStringEnumerator { 90 | // @Suppress("UnstableApiUsage") 91 | // return PersistentStringEnumerator(enumeratorPath, ENUMERATOR_INITIAL_SIZE, true, StorageLockContext(true)) 92 | // } 93 | // 94 | // private fun recreateEnumerator() { 95 | // IOUtil.closeSafe(LOGGER, enumerator) 96 | // IOUtil.deleteAllFilesStartingWith(enumeratorPath.toFile()) 97 | // enumerator = createEnumerator() 98 | // } 99 | 100 | private fun readString(input: DataInput): String { 101 | return input.readUTF().intern() 102 | // return enumerator.valueOf(DataInputOutputUtil.readINT(input))?.intern() 103 | // ?: throw IOException("Invalid enumerated string") 104 | } 105 | 106 | @Suppress("TooGenericExceptionCaught") 107 | private fun writeString(output: DataOutput, value: String) { 108 | output.writeUTF(value) 109 | // try { 110 | // DataInputOutputUtil.writeINT(output, enumerator.enumerate(value)) 111 | // } catch (e: Throwable) { 112 | // recreateEnumerator() 113 | // FileBasedIndex.getInstance().requestRebuild(INDEX_ID, e) 114 | // throw e 115 | // } 116 | } 117 | 118 | // @Suppress("UnstableApiUsage") 119 | // override fun createIndexImplementation( 120 | // extension: FileBasedIndexExtension>>, 121 | // indexStorageLayout: VfsAwareIndexStorageLayout>> 122 | // ) = object : VfsAwareMapReduceIndex>>(extension, indexStorageLayout, null) { 123 | // override fun doClear() { 124 | // super.doClear() 125 | // recreateEnumerator() 126 | // } 127 | // 128 | // override fun doFlush() { 129 | // super.doFlush() 130 | // enumerator.force() 131 | // } 132 | // 133 | // override fun doDispose() { 134 | // try { 135 | // super.doDispose() 136 | // } finally { 137 | // IOUtil.closeSafe(LOGGER, enumerator) 138 | // } 139 | // } 140 | // } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/ClassLocator.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.psi.PsiElement 4 | import com.intellij.psi.PsiReferenceList 5 | import com.intellij.psi.PsiTypeElement 6 | 7 | class ClassLocator( 8 | internalName: String, 9 | className: String, 10 | location: String, 11 | index: Int 12 | ) : DecompiledSourceElementLocator(className, location, index) { 13 | private val descriptor = "L$internalName;" 14 | 15 | override fun visitTypeElement(typeElement: PsiTypeElement) { 16 | super.visitTypeElement(typeElement) 17 | 18 | if (isDescriptorOfType(descriptor, typeElement.type)) { 19 | matchElement(typeElement) 20 | } 21 | } 22 | 23 | override fun visitReferenceList(list: PsiReferenceList) { 24 | super.visitReferenceList(list) 25 | 26 | for ((element, type) in list.referenceElements.zip(list.referencedTypes)) { 27 | if (isDescriptorOfType(descriptor, type)) { 28 | matchElement(element) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/DecompiledSourceElementLocator.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.psi.JavaRecursiveElementVisitor 4 | import com.intellij.psi.PsiAnonymousClass 5 | import com.intellij.psi.PsiClass 6 | import com.intellij.psi.PsiClassInitializer 7 | import com.intellij.psi.PsiElement 8 | import com.intellij.psi.PsiEnumConstant 9 | import com.intellij.psi.PsiExpressionStatement 10 | import com.intellij.psi.PsiField 11 | import com.intellij.psi.PsiMethod 12 | import com.intellij.psi.PsiMethodCallExpression 13 | import com.intellij.psi.PsiModifier 14 | import com.intellij.psi.PsiRecordComponent 15 | import com.intellij.psi.util.PsiTreeUtil 16 | import com.intellij.psi.util.PsiUtil 17 | 18 | open class DecompiledSourceElementLocator( 19 | val className: String, 20 | private val location: String, 21 | val index: Int 22 | ) : JavaRecursiveElementVisitor() { 23 | private class ClassScope(val className: String, var anonymousClassIndex: Int = 0) 24 | 25 | private val classScopeStack = java.util.ArrayDeque() 26 | private var foundElement: T? = null 27 | private var foundCount = 0 28 | val locationName = location.substringBefore(':') 29 | val locationDesc = location.substringAfter(':') 30 | val locationIsMethod = locationDesc.contains("(") 31 | private var constructorCallsThis = false 32 | 33 | override fun toString() = "${javaClass.simpleName}($className, $location, $index)" 34 | 35 | open fun findElement(clazz: PsiClass): T? { 36 | foundElement = null 37 | foundCount = 0 38 | constructorCallsThis = false 39 | withSlowOperationsIfNecessary { 40 | clazz.accept(this) 41 | } 42 | return foundElement 43 | } 44 | 45 | protected fun matchElement(element: T) { 46 | if (isInLocation(element) && foundCount++ == index) { 47 | foundElement = element 48 | } 49 | } 50 | 51 | private fun isInClassLocation(): Boolean { 52 | return classScopeStack.descendingIterator().asSequence() 53 | .joinToString("\$") { it.className } == className 54 | } 55 | 56 | private fun isInLocation(element: PsiElement): Boolean { 57 | val parent = PsiTreeUtil.getParentOfType( 58 | element, 59 | PsiMethod::class.java, 60 | PsiField::class.java, 61 | PsiRecordComponent::class.java, 62 | PsiClass::class.java, 63 | PsiClassInitializer::class.java 64 | ) ?: return false 65 | if (!isInClassLocation()) { 66 | return false 67 | } 68 | when (parent) { 69 | is PsiMethod -> { 70 | if (!locationIsMethod) { 71 | return false 72 | } 73 | if (parent.isConstructor) { 74 | if (locationName != "") { 75 | return false 76 | } 77 | } else { 78 | if (locationName != parent.name) { 79 | return false 80 | } 81 | } 82 | return isDescriptorOfMethodType(locationDesc, parent) 83 | } 84 | is PsiField -> { 85 | val initializer = parent.initializer 86 | val enumConstantArgs = (parent as? PsiEnumConstant)?.argumentList 87 | val isInInitializer = !PsiUtil.isCompileTimeConstant(parent) && 88 | ( 89 | (initializer != null && PsiTreeUtil.isAncestor(initializer, element, false)) || 90 | (enumConstantArgs != null && PsiTreeUtil.isAncestor(enumConstantArgs, element, false)) 91 | ) 92 | if (isInInitializer) { 93 | if (!locationIsMethod) { 94 | return false 95 | } 96 | val isStatic = parent.hasModifierProperty(PsiModifier.STATIC) || parent is PsiEnumConstant 97 | return if (isStatic) { 98 | locationName == "" 99 | } else { 100 | locationName == "" && !constructorCallsThis 101 | } 102 | } else { 103 | if (locationIsMethod || locationName.isEmpty()) { 104 | return false 105 | } 106 | return parent.name == locationName && isDescriptorOfType(locationDesc, parent.type) 107 | } 108 | } 109 | is PsiRecordComponent -> { 110 | if (locationIsMethod || locationName.isEmpty()) { 111 | return false 112 | } 113 | return parent.name == locationName && isDescriptorOfType(locationDesc, parent.type) 114 | } 115 | is PsiClass -> { 116 | return locationName.isEmpty() 117 | } 118 | is PsiClassInitializer -> { 119 | if (!locationIsMethod) { 120 | return false 121 | } 122 | val isStatic = parent.hasModifierProperty(PsiModifier.STATIC) 123 | return if (isStatic) { 124 | locationName == "" 125 | } else { 126 | locationName == "" && !constructorCallsThis 127 | } 128 | } 129 | else -> throw AssertionError() 130 | } 131 | } 132 | 133 | override fun visitAnonymousClass(clazz: PsiAnonymousClass) { 134 | classScopeStack.push(ClassScope("${++classScopeStack.peek().anonymousClassIndex}")) 135 | try { 136 | super.visitAnonymousClass(clazz) 137 | } finally { 138 | classScopeStack.pop() 139 | } 140 | } 141 | 142 | override fun visitClass(clazz: PsiClass) { 143 | classScopeStack.push(ClassScope(clazz.name ?: return)) 144 | try { 145 | if (locationIsMethod && locationName == "" && isInClassLocation()) { 146 | for (constructor in clazz.constructors) { 147 | if (isDescriptorOfMethodType(locationDesc, constructor)) { 148 | val firstStatement = constructor.body?.statements?.getOrNull(0) ?: break 149 | val firstExpression = (firstStatement as? PsiExpressionStatement)?.expression ?: break 150 | val firstMethodExpression = (firstExpression as? PsiMethodCallExpression)?.methodExpression ?: break 151 | val methodName = firstMethodExpression.referenceName ?: break 152 | if (methodName == "this") { 153 | constructorCallsThis = true 154 | } 155 | break 156 | } 157 | } 158 | } 159 | super.visitClass(clazz) 160 | } finally { 161 | classScopeStack.pop() 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/FakeDecompiledElement.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.ide.highlighter.JavaHighlightingColors 4 | import com.intellij.ide.util.EditorHelper 5 | import com.intellij.openapi.diagnostic.Logger 6 | import com.intellij.openapi.editor.markup.TextAttributes 7 | import com.intellij.openapi.util.TextRange 8 | import com.intellij.pom.Navigatable 9 | import com.intellij.psi.PsiCompiledFile 10 | import com.intellij.psi.PsiElement 11 | import com.intellij.psi.PsiJavaFile 12 | import com.intellij.psi.PsiReference 13 | import com.intellij.psi.PsiReferenceBase 14 | import com.intellij.psi.impl.FakePsiElement 15 | import com.intellij.usageView.UsageTreeColors 16 | import com.intellij.usageView.UsageTreeColorsScheme 17 | import com.intellij.usageView.UsageViewUtil 18 | import com.intellij.usages.TextChunk 19 | import com.intellij.usages.UsageInfo2UsageAdapter 20 | import com.intellij.usages.impl.UsagePreviewPanel 21 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Type 22 | import java.util.stream.Collectors 23 | 24 | open class FakeDecompiledElement( 25 | private val id: Int, 26 | protected val file: PsiCompiledFile, 27 | private val myParent: PsiElement, 28 | private val locator: DecompiledSourceElementLocator, 29 | ) : FakePsiElement(), Navigatable, IHasNavigationOffset, IHasCustomDescription { 30 | 31 | companion object { 32 | private val LOGGER = Logger.getInstance(FakeDecompiledElement::class.java) 33 | private val USAGE_VIEW_UTIL: String = UsageViewUtil::class.java.name 34 | private val USAGE_INFO_2_UTIL_ADAPTER: String = UsageInfo2UsageAdapter::class.java.name 35 | private val USAGE_PREVIEW_PANEL: String = UsagePreviewPanel::class.java.name 36 | } 37 | 38 | fun createReference(target: PsiElement): PsiReference { 39 | val self = this 40 | val ref = PsiReferenceBase.createSelfReference(this, target) 41 | return object : PsiReference by ref { 42 | override fun getRangeInElement() = self.textRange 43 | } 44 | } 45 | 46 | override fun getParent() = myParent 47 | 48 | override fun navigate(requestFocus: Boolean) { 49 | val result = findElement() ?: return 50 | val navigatable = result as? Navigatable 51 | if (navigatable != null) { 52 | if (navigatable.canNavigate()) { 53 | navigatable.navigate(requestFocus) 54 | } 55 | } else { 56 | EditorHelper.openInEditor(result) 57 | } 58 | } 59 | 60 | override fun canNavigate() = true 61 | 62 | override fun getTextRange() = getTextRange(false) 63 | 64 | private fun getTextRange(shiftForCursor: Boolean): TextRange { 65 | val stackFrames = StackWalker.getInstance().walk { stream -> 66 | stream.dropWhile { !it.className.startsWith("com.intellij.") } 67 | .dropWhile { it.methodName == "getNavigationOffset" || it.methodName == "getNavigationRange" } 68 | .limit(2) 69 | .collect(Collectors.toList()) 70 | } ?: return TextRange(id * 2, id * 2 + 1) 71 | val reason = stackFrames.firstOrNull() ?: return TextRange(id * 2, id * 2 + 1) 72 | val reasonClass = reason.className 73 | val methodName = reason.methodName 74 | val isHighlightMethod = reasonClass == USAGE_PREVIEW_PANEL && methodName == "highlight" 75 | return if ((reasonClass == USAGE_VIEW_UTIL && methodName == "navigateTo") || 76 | (reasonClass == USAGE_INFO_2_UTIL_ADAPTER && methodName == "getDescriptor") || 77 | isHighlightMethod 78 | ) { 79 | val element = findElement() ?: return TextRange(id * 2, id * 2 + 1) 80 | val range = element.textRange ?: return TextRange(id * 2, id * 2 + 1) 81 | val secondFrame = stackFrames.getOrNull(1) ?: return TextRange(id * 2, id * 2 + 1) 82 | if (shiftForCursor || 83 | isHighlightMethod || 84 | (secondFrame.className == USAGE_INFO_2_UTIL_ADAPTER && secondFrame.methodName == "openTextEditor") 85 | ) { 86 | range.shiftRight(element.textOffset - range.startOffset) 87 | } else { 88 | range 89 | } 90 | } else { 91 | TextRange(id * 2, id * 2 + 1) 92 | } 93 | } 94 | 95 | override fun getTextRangeInParent() = textRange 96 | 97 | override fun getTextLength() = 1 98 | 99 | override fun getTextOffset() = getTextRange(true).startOffset 100 | 101 | override fun getText() = "A" 102 | 103 | override fun getLineNumber() = id 104 | 105 | override fun getNavigationOffset() = getTextRange(true).startOffset 106 | 107 | override fun getCustomDescription(): Array { 108 | val colorScheme = UsageTreeColorsScheme.getInstance().scheme 109 | val ret = mutableListOf(TextChunk(UsageTreeColors.NUMBER_OF_USAGES_ATTRIBUTES.toTextAttributes(), "#${locator.index + 1}")) 110 | 111 | fun makePresentableType(type: Type): List { 112 | val plainType = if (type.sort == Type.ARRAY) { 113 | type.elementType 114 | } else { 115 | type 116 | } 117 | val plainSimpleName = plainType.className.split('.', '$').last() 118 | val plainAttr = if (plainType.isPrimitive()) { 119 | colorScheme.getAttributes(JavaHighlightingColors.KEYWORD) 120 | } else { 121 | colorScheme.getAttributes(JavaHighlightingColors.CLASS_NAME_ATTRIBUTES) 122 | } 123 | return when (type.sort) { 124 | Type.ARRAY -> listOf( 125 | TextChunk(plainAttr, plainSimpleName), 126 | TextChunk(colorScheme.getAttributes(JavaHighlightingColors.BRACKETS), "[]".repeat(type.dimensions)) 127 | ) 128 | else -> listOf(TextChunk(plainAttr, plainSimpleName)) 129 | } 130 | } 131 | 132 | val methodType = if (locator.locationIsMethod) Type.getMethodType(locator.locationDesc) else null 133 | 134 | when (locator.locationName) { 135 | "" -> { 136 | ret += TextChunk(TextAttributes(), "Class scope") 137 | } 138 | "" -> { 139 | ret += TextChunk(colorScheme.getAttributes(JavaHighlightingColors.KEYWORD), "static") 140 | ret += TextChunk(TextAttributes(), " ") 141 | ret += TextChunk(colorScheme.getAttributes(JavaHighlightingColors.BRACES), "{}") 142 | } 143 | "" -> { 144 | ret += TextChunk( 145 | colorScheme.getAttributes( 146 | JavaHighlightingColors.CONSTRUCTOR_DECLARATION_ATTRIBUTES 147 | ), 148 | locator.className.replace('$', '.') 149 | ) 150 | } 151 | else -> { 152 | if (methodType != null) { 153 | ret.addAll(makePresentableType(methodType.returnType)) 154 | ret += TextChunk(TextAttributes(), " ") 155 | } else if (locator.locationDesc.isNotEmpty()) { 156 | ret.addAll(makePresentableType(Type.getType(locator.locationDesc))) 157 | ret += TextChunk(TextAttributes(), " ") 158 | } 159 | 160 | val nameAttr = if (methodType != null) { 161 | colorScheme.getAttributes(JavaHighlightingColors.METHOD_DECLARATION_ATTRIBUTES) 162 | } else { 163 | colorScheme.getAttributes(JavaHighlightingColors.INSTANCE_FIELD_ATTRIBUTES) 164 | } 165 | ret += TextChunk(nameAttr, locator.locationName) 166 | } 167 | } 168 | 169 | if (methodType != null && locator.locationName != "") { 170 | ret += TextChunk(colorScheme.getAttributes(JavaHighlightingColors.PARENTHESES), "(") 171 | val argTypes = methodType.argumentTypes 172 | argTypes.asSequence().map { 173 | makePresentableType(it) 174 | }.withIndex().flatMap { (index, it) -> 175 | if (index == 0) { 176 | sequenceOf(it) 177 | } else { 178 | sequenceOf( 179 | listOf( 180 | TextChunk(colorScheme.getAttributes(JavaHighlightingColors.COMMA), ","), 181 | TextChunk(TextAttributes(), " ") 182 | ), 183 | it 184 | ) 185 | } 186 | }.flatMap { it.asSequence() } 187 | .forEach { ret += it } 188 | ret += TextChunk(colorScheme.getAttributes(JavaHighlightingColors.PARENTHESES), ")") 189 | } 190 | 191 | return ret.toTypedArray() 192 | } 193 | 194 | private fun findElement(): T? { 195 | val clazz = (file.decompiledPsiFile as? PsiJavaFile)?.classes?.firstOrNull() 196 | if (clazz == null) { 197 | LOGGER.warn("Could not find class inside PsiCompiledFile") 198 | return null 199 | } 200 | val foundElement = locator.findElement(clazz) 201 | if (foundElement == null) { 202 | LOGGER.warn("Could not locate element at $locator") 203 | } 204 | return foundElement 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/FieldLocator.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.psi.PsiClass 4 | import com.intellij.psi.PsiElement 5 | import com.intellij.psi.PsiField 6 | import com.intellij.psi.PsiReferenceExpression 7 | import com.intellij.psi.SmartPsiElementPointer 8 | import com.intellij.psi.util.PsiUtil 9 | 10 | class FieldLocator( 11 | private val fieldPtr: SmartPsiElementPointer, 12 | private val isWrite: Boolean, 13 | className: String, 14 | location: String, 15 | index: Int 16 | ) : DecompiledSourceElementLocator(className, location, index) { 17 | private var field: PsiField? = null 18 | 19 | override fun findElement(clazz: PsiClass): PsiElement? { 20 | field = fieldPtr.element ?: return null 21 | try { 22 | return super.findElement(clazz) 23 | } finally { 24 | field = null 25 | } 26 | } 27 | 28 | override fun visitReferenceExpression(expression: PsiReferenceExpression) { 29 | super.visitReferenceExpression(expression) 30 | if (PsiUtil.isAccessedForWriting(expression) == isWrite) { 31 | if (expression.isReferenceTo(field!!)) { 32 | matchElement(expression) 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/IHasCustomDescription.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.usages.TextChunk 4 | 5 | interface IHasCustomDescription { 6 | fun getCustomDescription(): Array 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/IHasNavigationOffset.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | interface IHasNavigationOffset { 4 | fun getNavigationOffset(): Int 5 | fun getLineNumber(): Int 6 | } 7 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/IIsWriteOverride.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | interface IIsWriteOverride { 4 | fun isWrite(): Boolean 5 | } 6 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/ImplicitToStringLocator.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.psi.CommonClassNames 4 | import com.intellij.psi.JavaTokenType 5 | import com.intellij.psi.PsiAssignmentExpression 6 | import com.intellij.psi.PsiClass 7 | import com.intellij.psi.PsiClassType 8 | import com.intellij.psi.PsiExpression 9 | import com.intellij.psi.PsiPolyadicExpression 10 | import com.intellij.psi.PsiType 11 | import com.intellij.psi.SmartPsiElementPointer 12 | import com.intellij.psi.util.InheritanceUtil 13 | 14 | class ImplicitToStringLocator( 15 | private val baseClassPtr: SmartPsiElementPointer, 16 | className: String, 17 | location: String, 18 | index: Int 19 | ) : DecompiledSourceElementLocator(className, location, index) { 20 | private var baseClass: PsiClass? = null 21 | 22 | override fun findElement(clazz: PsiClass): PsiExpression? { 23 | baseClass = baseClassPtr.element ?: return null 24 | try { 25 | return super.findElement(clazz) 26 | } finally { 27 | baseClass = null 28 | } 29 | } 30 | 31 | private fun isOurType(type: PsiType): Boolean { 32 | val resolved = (type as? PsiClassType)?.resolve() ?: return false 33 | return InheritanceUtil.isInheritorOrSelf(resolved, baseClass, true) 34 | } 35 | 36 | override fun visitPolyadicExpression(expression: PsiPolyadicExpression) { 37 | if (expression.operationTokenType == JavaTokenType.PLUS) { 38 | val resultType = expression.type 39 | if (resultType != null && resultType.equalsToText(CommonClassNames.JAVA_LANG_STRING)) { 40 | for (operand in expression.operands) { 41 | val operandType = operand.type 42 | if (operandType != null && isOurType(operandType)) { 43 | matchElement(expression) 44 | } 45 | } 46 | } 47 | } 48 | super.visitPolyadicExpression(expression) 49 | } 50 | 51 | override fun visitAssignmentExpression(expression: PsiAssignmentExpression) { 52 | if (expression.operationTokenType == JavaTokenType.PLUSEQ) { 53 | val leftType = expression.lExpression.type 54 | if (leftType != null && leftType.equalsToText(CommonClassNames.JAVA_LANG_STRING)) { 55 | val rightType = expression.rExpression?.type 56 | if (rightType != null && isOurType(rightType)) { 57 | matchElement(expression) 58 | } 59 | } 60 | } 61 | super.visitAssignmentExpression(expression) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/ImplicitToStringSearchExtension.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.openapi.vfs.VirtualFile 4 | import com.intellij.psi.CommonClassNames 5 | import com.intellij.psi.JavaPsiFacade 6 | import com.intellij.psi.PsiClass 7 | import com.intellij.psi.PsiCompiledFile 8 | import com.intellij.psi.PsiExpression 9 | import com.intellij.psi.PsiType 10 | import com.intellij.psi.SmartPointerManager 11 | import com.intellij.psi.search.searches.ClassInheritorsSearch 12 | import com.intellij.psi.search.searches.ImplicitToStringSearch 13 | import com.intellij.util.Processor 14 | import com.intellij.util.QueryExecutor 15 | 16 | class ImplicitToStringSearchExtension : QueryExecutor { 17 | override fun execute( 18 | queryParameters: ImplicitToStringSearch.SearchParameters, 19 | consumer: Processor 20 | ): Boolean { 21 | runReadActionInSmartModeWithWritePriority( 22 | queryParameters.targetMethod.project, 23 | { 24 | queryParameters.targetMethod.isValid 25 | } 26 | ) scope@{ 27 | val files = mutableMapOf>() 28 | val declaringClass = queryParameters.targetMethod.containingClass ?: return@scope 29 | addFiles(declaringClass, queryParameters, files) 30 | for (inheritor in ClassInheritorsSearch.search(declaringClass)) { 31 | addFiles(inheritor, queryParameters, files) 32 | } 33 | val baseClassPtr = SmartPointerManager.createPointer(declaringClass) 34 | var id = 0 35 | for ((file, occurrences) in files) { 36 | val psiFile = findCompiledFileWithoutSources(declaringClass.project, file) ?: continue 37 | for ((location, count) in occurrences) { 38 | repeat(count) { i -> 39 | consumer.process( 40 | ImplicitToStringElement( 41 | id++, 42 | psiFile, 43 | ImplicitToStringLocator(baseClassPtr, file.nameWithoutExtension, location, i) 44 | ) 45 | ) 46 | } 47 | } 48 | } 49 | } 50 | return true 51 | } 52 | 53 | private fun addFiles( 54 | owningClass: PsiClass, 55 | queryParameters: ImplicitToStringSearch.SearchParameters, 56 | files: MutableMap> 57 | ) { 58 | val internalName = owningClass.internalName ?: return 59 | val results = ClassFileIndex.search(internalName, ImplicitToStringKey.INSTANCE, queryParameters.searchScope) 60 | for ((file, sourceMap) in results) { 61 | val targetMap = files.computeIfAbsent(file) { mutableMapOf() } 62 | for ((k, v) in sourceMap) { 63 | targetMap.merge(k, v, Integer::sum) 64 | } 65 | } 66 | } 67 | 68 | class ImplicitToStringElement( 69 | id: Int, 70 | file: PsiCompiledFile, 71 | locator: DecompiledSourceElementLocator 72 | ) : FakeDecompiledElement(id, file, file, locator), PsiExpression { 73 | override fun getType(): PsiType { 74 | return JavaPsiFacade.getElementFactory(file.project).createTypeByFQClassName(CommonClassNames.JAVA_LANG_STRING) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/IndexerAnnotationVisitor.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.AnnotationVisitor 4 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Opcodes 5 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Type 6 | 7 | class IndexerAnnotationVisitor(private val cv: IndexerClassVisitor) : AnnotationVisitor(Opcodes.ASM9) { 8 | override fun visit(name: String?, value: Any?) { 9 | cv.addConstant(value) 10 | } 11 | 12 | override fun visitEnum(name: String?, descriptor: String, value: String) { 13 | cv.addFieldRef(Type.getType(descriptor).internalName, value, false) 14 | } 15 | 16 | override fun visitAnnotation(name: String?, descriptor: String): AnnotationVisitor { 17 | cv.addTypeDescriptor(descriptor) 18 | return this 19 | } 20 | 21 | override fun visitArray(name: String?): AnnotationVisitor { 22 | return this 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/IndexerClassVisitor.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | import com.intellij.openapi.progress.ProgressManager 5 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.AnnotationVisitor 6 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.ClassVisitor 7 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.ConstantDynamic 8 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.FieldVisitor 9 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Handle 10 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.MethodVisitor 11 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Opcodes 12 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.RecordComponentVisitor 13 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Type 14 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.TypePath 15 | 16 | class IndexerClassVisitor : ClassVisitor(Opcodes.ASM9) { 17 | lateinit var className: String 18 | val index = SmartMap>>() 19 | val locationStack = java.util.ArrayDeque() 20 | 21 | private val lambdaLocationMappings = mutableMapOf>() 22 | private val syntheticMethods = mutableSetOf() 23 | 24 | fun addRef(name: String, key: BinaryIndexKey) { 25 | ProgressManager.checkCanceled() 26 | index.computeIfAbsent(name.intern()) { SmartMap() }.computeIfAbsent(key) { SmartMap() }.merge(locationStack.peek(), 1, Integer::sum) 27 | } 28 | fun addClassRef(name: String) { 29 | addRef(name, ClassIndexKey.INSTANCE) 30 | } 31 | fun addFieldRef(owner: String, name: String, isWrite: Boolean) { 32 | addRef(name, FieldIndexKey(owner.intern(), isWrite)) 33 | } 34 | fun addMethodRef(owner: String, name: String, desc: String) { 35 | addRef(name, MethodIndexKey(owner.intern(), desc.intern())) 36 | } 37 | fun addDelegateRef(name: String, key: BinaryIndexKey) { 38 | index[name]?.get(key)?.remove(locationStack.peek()) 39 | addRef(name, DelegateIndexKey(key)) 40 | } 41 | 42 | fun addLambdaLocationMapping(lambdaLocation: String) { 43 | lambdaLocationMappings.computeIfAbsent(lambdaLocation) { mutableMapOf() }.merge(locationStack.peek(), 1, Integer::sum) 44 | } 45 | 46 | fun addTypeDescriptor(desc: String) { 47 | var type = Type.getType(desc) 48 | while (type.sort == Type.ARRAY) { 49 | type = type.elementType 50 | } 51 | if (type.sort == Type.OBJECT) { 52 | addClassRef(type.internalName) 53 | } 54 | } 55 | 56 | // fun addStringConstant(cst: String) { 57 | // // addRef(cst, StringConstantKey.INSTANCE) 58 | // } 59 | fun addConstant(cst: Any?) { 60 | if (cst == null) return 61 | when (cst) { 62 | // is String -> addStringConstant(cst) TODO 63 | is Type -> addTypeDescriptor(cst.descriptor) 64 | is Handle -> { 65 | when (cst.tag) { 66 | Opcodes.H_GETFIELD, Opcodes.H_GETSTATIC -> { 67 | addFieldRef(cst.owner, cst.name, false) 68 | } 69 | Opcodes.H_PUTFIELD, Opcodes.H_PUTSTATIC -> { 70 | addFieldRef(cst.owner, cst.name, true) 71 | } 72 | else -> { 73 | addMethodRef(cst.owner, cst.name, cst.desc) 74 | } 75 | } 76 | } 77 | is ConstantDynamic -> { 78 | val bootstrapMethodArguments = (0 until cst.bootstrapMethodArgumentCount).map { cst.getBootstrapMethodArgument(it) }.toTypedArray() 79 | IndexerMethodVisitor(this, 0, "()V").visitInvokeDynamicInsn(cst.name, cst.descriptor, cst.bootstrapMethod, *bootstrapMethodArguments) 80 | } 81 | else -> { 82 | if (cst.javaClass.isArray) { 83 | for (i in 0 until java.lang.reflect.Array.getLength(cst)) { 84 | addConstant(java.lang.reflect.Array.get(cst, i)) 85 | } 86 | } 87 | } 88 | } 89 | } 90 | 91 | private fun addClassSignature(sig: String) { 92 | addFormalTypeParameters(sig) 93 | } 94 | private fun addFormalTypeParameters(sig: String): Int { 95 | if (sig.isEmpty() || sig[0] != '<') return 0 96 | var i = 1 97 | while (i < sig.length && sig[i] != '>') { 98 | val prevI = i 99 | i = addFormalTypeParameter(sig, i) 100 | if (i == prevI) return i 101 | } 102 | if (i < sig.length) i++ 103 | return i 104 | } 105 | private fun addFormalTypeParameter(sig: String, ind: Int): Int { 106 | var i = ind 107 | i = readIdentifier(sig, i).second 108 | if (i >= sig.length || sig[i] != ':') return i 109 | i++ 110 | i = addFieldTypeSignature(sig, i, false) 111 | while (i < sig.length && sig[i] == ':') { 112 | i++ 113 | i = addFieldTypeSignature(sig, i, false) 114 | } 115 | return i 116 | } 117 | fun addFieldTypeSignature(sig: String, ind: Int, skipOuterName: Boolean): Int { 118 | if (ind == sig.length) return ind 119 | return when (sig[ind]) { 120 | 'L' -> addClassTypeSignature(sig, ind, skipOuterName) 121 | '[' -> addArrayTypeSignature(sig, ind, skipOuterName) 122 | 'T' -> addTypeVariableSignature(sig, ind) 123 | else -> ind 124 | } 125 | } 126 | private fun addClassTypeSignature(sig: String, ind: Int, skipOuterName: Boolean): Int { 127 | if (ind >= sig.length || sig[ind] != 'L') return ind 128 | var i = ind + 1 129 | val (id, nextI) = readIdentifier(sig, i) 130 | var qualifiedName = id 131 | i = nextI 132 | while (i < sig.length && sig[i] == '/') { 133 | i++ 134 | qualifiedName += "/" 135 | val (id2, nextI2) = readIdentifier(sig, i) 136 | qualifiedName += id2 137 | i = nextI2 138 | } 139 | if (i < sig.length && sig[i] == '<') { 140 | i = addTypeArguments(sig, i) 141 | } 142 | while (i < sig.length && sig[i] == '.') { 143 | i++ 144 | qualifiedName += "$" 145 | val (id2, nextI2) = readIdentifier(sig, i) 146 | qualifiedName += id2 147 | i = nextI2 148 | if (i < sig.length && sig[i] == '<') { 149 | i = addTypeArguments(sig, i) 150 | } 151 | } 152 | if (i < sig.length && sig[i] == ';') { 153 | i++ 154 | } 155 | if (!skipOuterName) { 156 | addClassRef(qualifiedName) 157 | } 158 | return i 159 | } 160 | private fun addTypeArguments(sig: String, ind: Int): Int { 161 | if (ind >= sig.length || sig[ind] != '<') return ind 162 | var i = ind + 1 163 | while (i < sig.length && sig[i] != '>') { 164 | val prevI = i 165 | i = addTypeArgument(sig, i) 166 | if (i == prevI) return i 167 | } 168 | if (i < sig.length) i++ 169 | return i 170 | } 171 | private fun addTypeArgument(sig: String, ind: Int): Int { 172 | if (ind >= sig.length) return ind 173 | if (sig[ind] == '*') return ind + 1 174 | var i = ind 175 | if (sig[i] == '+' || sig[i] == '-') i++ 176 | i = addFieldTypeSignature(sig, i, false) 177 | return i 178 | } 179 | private fun addArrayTypeSignature(sig: String, ind: Int, skipOuterName: Boolean): Int { 180 | var i = ind 181 | while (i < sig.length && sig[i] == '[') i++ 182 | return addTypeSignature(sig, i, skipOuterName) 183 | } 184 | private fun addTypeVariableSignature(sig: String, ind: Int): Int { 185 | if (ind >= sig.length || sig[ind] != 'T') return ind 186 | var i = ind + 1 187 | i = readIdentifier(sig, i).second 188 | if (i < sig.length && sig[i] == ';') i++ 189 | return i 190 | } 191 | private fun addTypeSignature(sig: String, ind: Int, skipOuterName: Boolean): Int { 192 | if (ind >= sig.length) return ind 193 | if (BASE_TYPE_CHARS.contains(sig[ind])) return ind + 1 194 | return addFieldTypeSignature(sig, ind, skipOuterName) 195 | } 196 | private fun addMethodTypeSignature(sig: String) { 197 | var i = addFormalTypeParameters(sig) 198 | if (i >= sig.length || sig[i] != '(') return 199 | i++ 200 | while (i < sig.length && sig[i] != ')') { 201 | val prevI = i 202 | i = addTypeSignature(sig, i, false) 203 | if (i == prevI) return 204 | } 205 | if (i < sig.length) i++ 206 | if (i >= sig.length) return 207 | if (sig[i] == 'V') i++ 208 | else i = addTypeSignature(sig, i, false) 209 | while (i < sig.length && sig[i] == '^') { 210 | i++ 211 | i = addFieldTypeSignature(sig, i, false) 212 | } 213 | } 214 | private fun readIdentifier(sig: String, i: Int): Pair { 215 | var endI = i 216 | while (endI < sig.length && !ILLEGAL_SIG_CHARS.contains(sig[endI])) { 217 | endI++ 218 | } 219 | return sig.substring(i, endI) to endI 220 | } 221 | 222 | override fun visit( 223 | version: Int, 224 | access: Int, 225 | name: String, 226 | signature: String?, 227 | superName: String?, 228 | interfaces: Array? 229 | ) { 230 | locationStack.push("") 231 | className = name 232 | signature?.let { addClassSignature(it) } 233 | superName?.let { addClassRef(it) } 234 | interfaces?.forEach { addClassRef(it) } 235 | } 236 | 237 | override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor { 238 | addTypeDescriptor(descriptor) 239 | return IndexerAnnotationVisitor(this) 240 | } 241 | 242 | override fun visitTypeAnnotation( 243 | typeRef: Int, 244 | typePath: TypePath?, 245 | descriptor: String, 246 | visible: Boolean 247 | ): AnnotationVisitor { 248 | addTypeDescriptor(descriptor) 249 | return IndexerAnnotationVisitor(this) 250 | } 251 | 252 | override fun visitRecordComponent(name: String, descriptor: String, signature: String?): RecordComponentVisitor { 253 | locationStack.push("$name:$descriptor") 254 | addTypeDescriptor(descriptor) 255 | signature?.let { addFieldTypeSignature(it, 0, true) } 256 | return IndexerRecordComponentVisitor(this) 257 | } 258 | 259 | override fun visitField( 260 | access: Int, 261 | name: String, 262 | descriptor: String, 263 | signature: String?, 264 | value: Any? 265 | ): FieldVisitor { 266 | locationStack.push("$name:$descriptor") 267 | addTypeDescriptor(descriptor) 268 | signature?.let { addFieldTypeSignature(it, 0, true) } 269 | addConstant(value) 270 | return IndexerFieldVisitor(this) 271 | } 272 | 273 | override fun visitMethod( 274 | access: Int, 275 | name: String, 276 | descriptor: String, 277 | signature: String?, 278 | exceptions: Array? 279 | ): MethodVisitor { 280 | locationStack.push("$name:$descriptor") 281 | if ((access and Opcodes.ACC_SYNTHETIC) != 0) { 282 | syntheticMethods.add(locationStack.peek()) 283 | } 284 | val desc = Type.getMethodType(descriptor) 285 | for (argumentType in desc.argumentTypes) { 286 | addTypeDescriptor(argumentType.descriptor) 287 | } 288 | addTypeDescriptor(desc.returnType.descriptor) 289 | signature?.let { addMethodTypeSignature(it) } 290 | exceptions?.forEach { addClassRef(it) } 291 | return IndexerMethodVisitor(this, access, descriptor) 292 | } 293 | 294 | override fun visitEnd() { 295 | locationStack.pop() 296 | propagateLambdaLocations() 297 | } 298 | 299 | private fun propagateLambdaLocations() { 300 | val referencedByLambda = lambdaLocationMappings.entries.asSequence() 301 | .flatMap { (k, vs) -> vs.asSequence().map { v -> java.util.AbstractMap.SimpleEntry(k, v.key) } } 302 | .groupByTo(mutableMapOf(), { it.value }) { it.key } 303 | var changed = true 304 | while (lambdaLocationMappings.isNotEmpty() && changed) { 305 | changed = false 306 | val lambdaLocItr = lambdaLocationMappings.iterator() 307 | while (lambdaLocItr.hasNext()) { 308 | val (lambdaLoc, targets) = lambdaLocItr.next() 309 | if (referencedByLambda[lambdaLoc]?.isNotEmpty() == true) { 310 | continue // something still needs to be inlined into this lambda, can't inline this one yet 311 | } 312 | changed = true 313 | lambdaLocItr.remove() 314 | for ((targetLoc, countOfLambda) in targets) { 315 | referencedByLambda[targetLoc]?.remove(lambdaLoc) 316 | for (keys in index.values) { 317 | for (locations in keys.values) { 318 | val countInLambda = locations.remove(lambdaLoc) ?: continue 319 | locations.merge(targetLoc, countOfLambda * countInLambda, Integer::sum) 320 | } 321 | } 322 | } 323 | } 324 | } 325 | 326 | if (lambdaLocationMappings.isNotEmpty()) { 327 | LOGGER.warn("$className: unable to propagate lambda locations") 328 | } 329 | } 330 | 331 | companion object { 332 | private val LOGGER = Logger.getInstance(IndexerClassVisitor::class.java) 333 | private val ILLEGAL_SIG_CHARS = setOf('.', ';', '[', '/', '<', '>', ':') 334 | private val BASE_TYPE_CHARS = setOf('B', 'C', 'D', 'F', 'I', 'J', 'S', 'Z') 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/IndexerFieldVisitor.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.AnnotationVisitor 4 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.FieldVisitor 5 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Opcodes 6 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.TypePath 7 | 8 | class IndexerFieldVisitor(private val cv: IndexerClassVisitor) : FieldVisitor(Opcodes.ASM9) { 9 | override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor { 10 | cv.addTypeDescriptor(descriptor) 11 | return IndexerAnnotationVisitor(cv) 12 | } 13 | 14 | override fun visitTypeAnnotation( 15 | typeRef: Int, 16 | typePath: TypePath?, 17 | descriptor: String, 18 | visible: Boolean 19 | ): AnnotationVisitor { 20 | cv.addTypeDescriptor(descriptor) 21 | return IndexerAnnotationVisitor(cv) 22 | } 23 | 24 | override fun visitEnd() { 25 | cv.locationStack.pop() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/IndexerMethodVisitor.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.AnnotationVisitor 4 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Handle 5 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Label 6 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.MethodVisitor 7 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Opcodes 8 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Type 9 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.TypePath 10 | 11 | class IndexerMethodVisitor( 12 | private val cv: IndexerClassVisitor, 13 | private val access: Int, 14 | private val desc: String 15 | ) : MethodVisitor(Opcodes.ASM9) { 16 | private val insns = mutableListOf() 17 | 18 | override fun visitAnnotationDefault(): AnnotationVisitor { 19 | return IndexerAnnotationVisitor(cv) 20 | } 21 | 22 | override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor { 23 | cv.addTypeDescriptor(descriptor) 24 | return IndexerAnnotationVisitor(cv) 25 | } 26 | 27 | override fun visitTypeAnnotation( 28 | typeRef: Int, 29 | typePath: TypePath?, 30 | descriptor: String, 31 | visible: Boolean 32 | ): AnnotationVisitor { 33 | cv.addTypeDescriptor(descriptor) 34 | return IndexerAnnotationVisitor(cv) 35 | } 36 | 37 | override fun visitParameterAnnotation(parameter: Int, descriptor: String, visible: Boolean): AnnotationVisitor { 38 | cv.addTypeDescriptor(descriptor) 39 | return IndexerAnnotationVisitor(cv) 40 | } 41 | 42 | override fun visitTypeInsn(opcode: Int, type: String) { 43 | var t = Type.getObjectType(type) 44 | while (t.sort == Type.ARRAY) { 45 | t = t.elementType 46 | } 47 | if (t.sort == Type.OBJECT) { 48 | cv.addClassRef(t.internalName) 49 | } 50 | insns += TypeInsn(opcode, type) 51 | } 52 | 53 | override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) { 54 | cv.addFieldRef(owner, name, opcode == Opcodes.PUTSTATIC || opcode == Opcodes.PUTFIELD) 55 | insns += MemberInsn(opcode, owner, name, descriptor) 56 | } 57 | 58 | override fun visitMethodInsn( 59 | opcode: Int, 60 | owner: String, 61 | name: String, 62 | descriptor: String, 63 | isInterface: Boolean 64 | ) { 65 | cv.addMethodRef(owner, name, descriptor) 66 | insns += MemberInsn(opcode, owner, name, descriptor) 67 | } 68 | 69 | override fun visitInvokeDynamicInsn( 70 | name: String?, 71 | descriptor: String, 72 | bootstrapMethodHandle: Handle, 73 | vararg bootstrapMethodArguments: Any? 74 | ) { 75 | when (bootstrapMethodHandle.owner) { 76 | "java/lang/invoke/LambdaMetafactory" -> { 77 | @Suppress("MagicNumber") 78 | val invokedMethod = when (bootstrapMethodHandle.name) { 79 | "metafactory" -> { 80 | if (bootstrapMethodArguments.size < 3) return 81 | bootstrapMethodArguments[1] as? Handle ?: return 82 | } 83 | "altMetafactory" -> { 84 | if (bootstrapMethodArguments.size < 2) return 85 | val extraArgs = bootstrapMethodArguments[0] as? Array<*> ?: return 86 | if (extraArgs.size < 2) return 87 | extraArgs[1] as? Handle ?: return 88 | } 89 | else -> return 90 | } 91 | if (invokedMethod.owner == cv.className) { 92 | cv.addLambdaLocationMapping("${invokedMethod.name}:${invokedMethod.desc}") 93 | } 94 | cv.addMethodRef(invokedMethod.owner, invokedMethod.name, invokedMethod.desc) 95 | } 96 | "java/lang/invoke/StringConcatFactory" -> { 97 | when (bootstrapMethodHandle.name) { 98 | "makeConcat", "makeConcatWithConstants" -> { 99 | val methodType = Type.getMethodType(descriptor) 100 | for (argumentType in methodType.argumentTypes) { 101 | if (argumentType.sort == Type.OBJECT) { 102 | cv.addRef(argumentType.internalName, ImplicitToStringKey.INSTANCE) 103 | } 104 | } 105 | // if (bootstrapMethodHandle.name == "makeConcatWithConstants") { 106 | // val recipe = bootstrapMethodArguments.getOrNull(0) as? String ?: return 107 | // recipe.split('\u0001', '\u0002').filter { it.isNotEmpty() } 108 | // .forEach { cv.addStringConstant(it) } 109 | // cv.addConstant(bootstrapMethodArguments.getOrNull(1)) 110 | // } 111 | } 112 | } 113 | } 114 | } 115 | insns += UnknownInsn(Opcodes.INVOKEDYNAMIC) 116 | } 117 | 118 | override fun visitLdcInsn(value: Any?) { 119 | cv.addConstant(value) 120 | insns += UnknownInsn(Opcodes.LDC) 121 | } 122 | 123 | override fun visitMultiANewArrayInsn(descriptor: String, numDimensions: Int) { 124 | cv.addTypeDescriptor(descriptor) 125 | insns += UnknownInsn(Opcodes.MULTIANEWARRAY) 126 | } 127 | 128 | override fun visitInsn(opcode: Int) { 129 | insns += NoOperandInsn(opcode) 130 | } 131 | 132 | override fun visitIntInsn(opcode: Int, operand: Int) { 133 | insns += UnknownInsn(opcode) 134 | } 135 | 136 | override fun visitVarInsn(opcode: Int, variable: Int) { 137 | insns += VarInsn(opcode, variable) 138 | } 139 | 140 | override fun visitJumpInsn(opcode: Int, label: Label?) { 141 | insns += UnknownInsn(opcode) 142 | } 143 | 144 | override fun visitIincInsn(variable: Int, increment: Int) { 145 | insns += UnknownInsn(Opcodes.IINC) 146 | } 147 | 148 | override fun visitTableSwitchInsn(min: Int, max: Int, dflt: Label?, vararg labels: Label?) { 149 | insns += UnknownInsn(Opcodes.TABLESWITCH) 150 | } 151 | 152 | override fun visitLookupSwitchInsn(dflt: Label?, keys: IntArray?, labels: Array?) { 153 | insns += UnknownInsn(Opcodes.LOOKUPSWITCH) 154 | } 155 | 156 | override fun visitInsnAnnotation( 157 | typeRef: Int, 158 | typePath: TypePath?, 159 | descriptor: String, 160 | visible: Boolean 161 | ): AnnotationVisitor { 162 | cv.addTypeDescriptor(descriptor) 163 | return IndexerAnnotationVisitor(cv) 164 | } 165 | 166 | override fun visitTryCatchBlock(start: Label?, end: Label?, handler: Label?, type: String?) { 167 | type?.let { cv.addClassRef(it) } 168 | } 169 | 170 | override fun visitTryCatchAnnotation( 171 | typeRef: Int, 172 | typePath: TypePath?, 173 | descriptor: String, 174 | visible: Boolean 175 | ): AnnotationVisitor { 176 | cv.addTypeDescriptor(descriptor) 177 | return IndexerAnnotationVisitor(cv) 178 | } 179 | 180 | override fun visitLocalVariable( 181 | name: String?, 182 | descriptor: String?, 183 | signature: String?, 184 | start: Label?, 185 | end: Label?, 186 | index: Int 187 | ) { 188 | signature?.let { cv.addFieldTypeSignature(it, 0, true) } 189 | } 190 | 191 | override fun visitLocalVariableAnnotation( 192 | typeRef: Int, 193 | typePath: TypePath?, 194 | start: Array?, 195 | end: Array?, 196 | index: IntArray?, 197 | descriptor: String, 198 | visible: Boolean 199 | ): AnnotationVisitor { 200 | cv.addTypeDescriptor(descriptor) 201 | return IndexerAnnotationVisitor(cv) 202 | } 203 | 204 | override fun visitEnd() { 205 | if ((access and Opcodes.ACC_SYNTHETIC) != 0) { 206 | checkSyntheticPattern() 207 | } 208 | cv.locationStack.pop() 209 | } 210 | 211 | private fun checkSyntheticPattern() { 212 | checkAccessMethod() 213 | } 214 | 215 | private fun checkAccessMethod() { 216 | val methodType = Type.getMethodType(desc) 217 | 218 | // load every parameter 219 | var varIndex = 0 220 | var insnIndex = 0 221 | if ((access and Opcodes.ACC_STATIC) == 0) { 222 | val insn = insns.firstOrNull() ?: return 223 | if (insn !is VarInsn || insn.variable != 0) return 224 | varIndex++ 225 | insnIndex++ 226 | } 227 | for (param in methodType.argumentTypes) { 228 | var insn = insns.getOrNull(insnIndex) ?: return 229 | // ignore checkcasts 230 | if (insn.opcode == Opcodes.CHECKCAST) insn = insns.getOrNull(++insnIndex) ?: return 231 | if (insn !is VarInsn || insn.variable != varIndex) return 232 | insnIndex++ 233 | if (param.sort == Type.DOUBLE || param.sort == Type.LONG) { 234 | varIndex += 2 235 | } else { 236 | varIndex++ 237 | } 238 | } 239 | 240 | // member access 241 | var insn = insns.getOrNull(insnIndex) ?: return 242 | // ignore checkcasts 243 | if (insn.opcode == Opcodes.CHECKCAST) insn = insns.getOrNull(++insnIndex) ?: return 244 | val memberInsn = insn as? MemberInsn ?: return 245 | insnIndex++ 246 | 247 | // return instruction 248 | insn = insns.getOrNull(insnIndex) ?: return 249 | // ignore checkcasts 250 | if (insn.opcode == Opcodes.CHECKCAST) insn = insns.getOrNull(++insnIndex) ?: return 251 | if (insn.opcode < Opcodes.IRETURN || insn.opcode > Opcodes.RETURN) return 252 | insnIndex++ 253 | 254 | // end of method 255 | if (insnIndex != insns.size) return 256 | 257 | when (memberInsn.opcode) { 258 | Opcodes.GETFIELD, Opcodes.GETSTATIC -> cv.addDelegateRef(memberInsn.name, FieldIndexKey(memberInsn.owner.intern(), false)) 259 | Opcodes.PUTFIELD, Opcodes.PUTSTATIC -> cv.addDelegateRef(memberInsn.name, FieldIndexKey(memberInsn.owner.intern(), true)) 260 | else -> cv.addDelegateRef(memberInsn.name, MethodIndexKey(memberInsn.owner.intern(), memberInsn.desc.intern())) 261 | } 262 | } 263 | 264 | private sealed class Insn(val opcode: Int) 265 | private class UnknownInsn(opcode: Int) : Insn(opcode) 266 | private class VarInsn(opcode: Int, val variable: Int) : Insn(opcode) 267 | private class MemberInsn(opcode: Int, val owner: String, val name: String, val desc: String) : Insn(opcode) 268 | private class TypeInsn(opcode: Int, type: String) : Insn(opcode) 269 | private class NoOperandInsn(opcode: Int) : Insn(opcode) 270 | } 271 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/IndexerRecordComponentVisitor.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.AnnotationVisitor 4 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Opcodes 5 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.RecordComponentVisitor 6 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.TypePath 7 | 8 | class IndexerRecordComponentVisitor(private val cv: IndexerClassVisitor) : RecordComponentVisitor(Opcodes.ASM9) { 9 | override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor { 10 | cv.addTypeDescriptor(descriptor) 11 | return IndexerAnnotationVisitor(cv) 12 | } 13 | 14 | override fun visitTypeAnnotation( 15 | typeRef: Int, 16 | typePath: TypePath?, 17 | descriptor: String, 18 | visible: Boolean 19 | ): AnnotationVisitor { 20 | cv.addTypeDescriptor(descriptor) 21 | return IndexerAnnotationVisitor(cv) 22 | } 23 | 24 | override fun visitEnd() { 25 | cv.locationStack.pop() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/MethodLocator.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.psi.PsiClass 4 | import com.intellij.psi.PsiElement 5 | import com.intellij.psi.PsiMethod 6 | import com.intellij.psi.PsiMethodCallExpression 7 | import com.intellij.psi.PsiMethodReferenceExpression 8 | import com.intellij.psi.PsiModifier 9 | import com.intellij.psi.PsiNewExpression 10 | import com.intellij.psi.SmartPsiElementPointer 11 | import com.intellij.psi.util.InheritanceUtil 12 | import com.intellij.psi.util.MethodSignatureUtil 13 | 14 | class MethodLocator( 15 | private val methodPtr: SmartPsiElementPointer, 16 | private val strict: Boolean, 17 | className: String, 18 | location: String, 19 | index: Int 20 | ) : DecompiledSourceElementLocator(className, location, index) { 21 | private var method: PsiMethod? = null 22 | 23 | override fun findElement(clazz: PsiClass): PsiElement? { 24 | method = methodPtr.element ?: return null 25 | try { 26 | return super.findElement(clazz) 27 | } finally { 28 | method = null 29 | } 30 | } 31 | 32 | private fun isOurMethod(theMethod: PsiMethod?): Boolean { 33 | if (theMethod == null) { 34 | return false 35 | } 36 | val method = this.method!! 37 | if (method.isConstructor != theMethod.isConstructor) { 38 | return false 39 | } 40 | if (method.isConstructor && method.containingClass != theMethod.containingClass) { 41 | return false 42 | } 43 | 44 | return if (strict) { 45 | method == theMethod || MethodSignatureUtil.isSuperMethod(method, theMethod) 46 | } else { 47 | if (method.name != theMethod.name) { 48 | return false 49 | } 50 | if (method.isConstructor) { 51 | // already checked our conditions 52 | return true 53 | } 54 | if (method.hasModifierProperty(PsiModifier.PRIVATE) || 55 | method.hasModifierProperty(PsiModifier.STATIC) 56 | ) { 57 | return method.containingClass == theMethod.containingClass 58 | } 59 | InheritanceUtil.isInheritorOrSelf(method.containingClass, method.containingClass, true) 60 | } 61 | } 62 | 63 | override fun visitNewExpression(expression: PsiNewExpression) { 64 | super.visitNewExpression(expression) 65 | if (isOurMethod(expression.resolveConstructor())) { 66 | matchElement(expression) 67 | } 68 | } 69 | 70 | override fun visitMethodCallExpression(expression: PsiMethodCallExpression) { 71 | super.visitMethodCallExpression(expression) 72 | if (isOurMethod(expression.resolveMethod())) { 73 | matchElement(expression) 74 | } 75 | } 76 | 77 | override fun visitMethodReferenceExpression(expression: PsiMethodReferenceExpression) { 78 | super.visitMethodReferenceExpression(expression) 79 | if (isOurMethod(expression.resolve() as? PsiMethod)) { 80 | matchElement(expression) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/MethodReferencesSearchExtension.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.psi.PsiCompiledFile 4 | import com.intellij.psi.PsiElement 5 | import com.intellij.psi.PsiModifier 6 | import com.intellij.psi.PsiReference 7 | import com.intellij.psi.PsiSubstitutor 8 | import com.intellij.psi.SmartPointerManager 9 | import com.intellij.psi.search.searches.ClassInheritorsSearch 10 | import com.intellij.psi.search.searches.MethodReferencesSearch 11 | import com.intellij.psi.util.MethodSignatureUtil 12 | import com.intellij.psi.util.TypeConversionUtil 13 | import com.intellij.util.Processor 14 | import com.intellij.util.QueryExecutor 15 | 16 | class MethodReferencesSearchExtension : QueryExecutor { 17 | override fun execute( 18 | queryParameters: MethodReferencesSearch.SearchParameters, 19 | consumer: Processor 20 | ): Boolean { 21 | runReadActionInSmartModeWithWritePriority(queryParameters.project, { queryParameters.isQueryValid }) scope@{ 22 | val method = queryParameters.method 23 | val methodDesc = method.descriptor ?: return@scope 24 | val declaringClass = method.containingClass ?: return@scope 25 | val internalName = declaringClass.internalName ?: return@scope 26 | val allowedOwners = mutableSetOf(internalName) 27 | val allowedDescs = mutableSetOf(methodDesc) 28 | val subMethodsHide = method.hasModifierProperty(PsiModifier.STATIC) 29 | if (!method.isConstructor && !method.hasModifierProperty(PsiModifier.PRIVATE) && !(subMethodsHide && declaringClass.isInterface)) { 30 | val classesWithHiddenMethods = mutableSetOf() 31 | derivedClassLoop@ 32 | for (derived in ClassInheritorsSearch.search(declaringClass)) { 33 | val derivedInternalName = derived.internalName ?: continue 34 | if (subMethodsHide) { 35 | var superType = derived.superClass 36 | while (superType != null) { 37 | val superInternalName = superType.internalName 38 | if (superInternalName != null) { 39 | if (classesWithHiddenMethods.contains(superInternalName)) { 40 | classesWithHiddenMethods += derivedInternalName 41 | continue@derivedClassLoop 42 | } 43 | if (superInternalName == internalName) { 44 | break 45 | } 46 | } 47 | superType = superType.superClass 48 | } 49 | } 50 | allowedOwners += derivedInternalName 51 | for (pair in derived.findMethodsAndTheirSubstitutorsByName(method.name, false)) { 52 | val parentSubstitutor = TypeConversionUtil.getSuperClassSubstitutor(declaringClass, derived, PsiSubstitutor.EMPTY) 53 | val parentSignature = method.getSignature(parentSubstitutor) 54 | val derivedMethod = pair.first 55 | val derivedSignature = derivedMethod.getSignature(pair.second) 56 | if (MethodSignatureUtil.isSubsignature(parentSignature, derivedSignature)) { 57 | if (subMethodsHide) { 58 | classesWithHiddenMethods += derivedInternalName 59 | } else { 60 | val derivedDesc = derivedMethod.descriptor 61 | if (derivedDesc != null) { 62 | allowedDescs += derivedDesc 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | val methodBinaryName = if (method.isConstructor) { 70 | "" 71 | } else { 72 | method.name 73 | } 74 | val files = ClassFileIndex.search( 75 | methodBinaryName, 76 | { key -> 77 | key is MethodIndexKey && 78 | allowedOwners.contains(key.owner) && 79 | (!queryParameters.isStrictSignatureSearch || allowedDescs.contains(key.desc)) 80 | }, 81 | queryParameters.effectiveSearchScope 82 | ) 83 | if (files.isEmpty()) { 84 | return@scope 85 | } 86 | val methodPtr = SmartPointerManager.createPointer(method) 87 | var id = 0 88 | for ((file, occurrences) in files) { 89 | val psiFile = findCompiledFileWithoutSources(queryParameters.project, file) ?: continue 90 | for ((location, count) in occurrences) { 91 | repeat(count) { i -> 92 | consumer.process( 93 | MethodRefElement( 94 | id++, 95 | psiFile, 96 | MethodLocator(methodPtr, queryParameters.isStrictSignatureSearch, file.nameWithoutExtension, location, i) 97 | ).createReference(method) 98 | ) 99 | } 100 | } 101 | } 102 | } 103 | return true 104 | } 105 | 106 | class MethodRefElement( 107 | id: Int, 108 | file: PsiCompiledFile, 109 | locator: DecompiledSourceElementLocator 110 | ) : FakeDecompiledElement(id, file, file, locator) 111 | } 112 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/ReferencesSearchExtension.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.intellij.openapi.vfs.VirtualFile 4 | import com.intellij.psi.PsiClass 5 | import com.intellij.psi.PsiCompiledFile 6 | import com.intellij.psi.PsiElement 7 | import com.intellij.psi.PsiField 8 | import com.intellij.psi.PsiReference 9 | import com.intellij.psi.SmartPointerManager 10 | import com.intellij.psi.search.SearchScope 11 | import com.intellij.psi.search.searches.ClassInheritorsSearch 12 | import com.intellij.psi.search.searches.ReferencesSearch 13 | import com.intellij.util.Processor 14 | import com.intellij.util.QueryExecutor 15 | 16 | class ReferencesSearchExtension : QueryExecutor { 17 | override fun execute( 18 | queryParameters: ReferencesSearch.SearchParameters, 19 | consumer: Processor 20 | ): Boolean { 21 | when (val element = queryParameters.elementToSearch) { 22 | is PsiField -> processField(element, queryParameters, consumer, queryParameters.effectiveSearchScope) 23 | is PsiClass -> processClass(element, queryParameters, consumer, queryParameters.effectiveSearchScope) 24 | } 25 | 26 | return true 27 | } 28 | 29 | private fun processField( 30 | element: PsiField, 31 | queryParameters: ReferencesSearch.SearchParameters, 32 | consumer: Processor, 33 | scope: SearchScope 34 | ) { 35 | runReadActionInSmartModeWithWritePriority(queryParameters.project, { queryParameters.isQueryValid }) scope@{ 36 | val fieldName = element.name 37 | val declaringClass = element.containingClass ?: return@scope 38 | val validOwnerNames = mutableSetOf(declaringClass.internalName) 39 | for (inheritor in ClassInheritorsSearch.search(declaringClass)) { 40 | validOwnerNames.add(inheritor.internalName) 41 | } 42 | val readFiles = mutableMapOf>() 43 | val writeFiles = mutableMapOf>() 44 | val results = ClassFileIndex.searchReturnKeys( 45 | fieldName, 46 | { key -> 47 | key is FieldIndexKey && validOwnerNames.contains(key.owner) 48 | }, 49 | scope 50 | ) 51 | if (results.isEmpty()) { 52 | return@scope 53 | } 54 | for ((file, keys) in results) { 55 | for ((key, value) in keys) { 56 | if ((key as FieldIndexKey).isWrite) { 57 | writeFiles[file] = value 58 | } else { 59 | readFiles[file] = value 60 | } 61 | } 62 | } 63 | val smartFieldPtr = SmartPointerManager.createPointer(element) 64 | var id = 0 65 | fun processFiles(files: Map>, isWrite: Boolean) { 66 | for ((file, occurrences) in files) { 67 | val psiFile = findCompiledFileWithoutSources(queryParameters.project, file) ?: continue 68 | for ((location, count) in occurrences) { 69 | repeat(count) { i -> 70 | consumer.process( 71 | FieldRefElement(id++, psiFile, FieldLocator(smartFieldPtr, isWrite, file.nameWithoutExtension, location, i), isWrite) 72 | .createReference(element) 73 | ) 74 | } 75 | } 76 | } 77 | } 78 | processFiles(readFiles, false) 79 | processFiles(writeFiles, true) 80 | } 81 | } 82 | 83 | private fun processClass( 84 | element: PsiClass, 85 | queryParameters: ReferencesSearch.SearchParameters, 86 | consumer: Processor, 87 | scope: SearchScope 88 | ) { 89 | runReadActionInSmartModeWithWritePriority(queryParameters.project, { queryParameters.isQueryValid }) scope@{ 90 | val internalName = element.internalName ?: return@scope 91 | val files = ClassFileIndex.search(internalName, ClassIndexKey.INSTANCE, scope) 92 | if (files.isEmpty()) { 93 | return@scope 94 | } 95 | var id = 0 96 | for ((file, occurrences) in files) { 97 | val psiFile = findCompiledFileWithoutSources(queryParameters.project, file) ?: continue 98 | for ((location, count) in occurrences) { 99 | repeat(count) { i -> 100 | consumer.process( 101 | ClassRefElement( 102 | id++, 103 | psiFile, 104 | ClassLocator(internalName, file.nameWithoutExtension, location, i) 105 | ).createReference(element) 106 | ) 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | class FieldRefElement( 114 | id: Int, 115 | file: PsiCompiledFile, 116 | locator: DecompiledSourceElementLocator, 117 | private val myIsWrite: Boolean 118 | ) : FakeDecompiledElement(id, file, file, locator), PsiElement, IIsWriteOverride { 119 | override fun isWrite() = myIsWrite 120 | } 121 | 122 | class ClassRefElement( 123 | id: Int, 124 | file: PsiCompiledFile, 125 | locator: DecompiledSourceElementLocator 126 | ) : FakeDecompiledElement(id, file, file, locator) 127 | } 128 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/SmartMap.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | // A mutable version of SmartFMap 4 | class SmartMap : AbstractMutableMap() { 5 | companion object { 6 | const val ARRAY_THRESHOLD = 8 7 | } 8 | 9 | private var value: Any = emptyArray() 10 | 11 | @Suppress("UNCHECKED_CAST") 12 | override fun put(key: K, value: V): V? { 13 | when (val thisVal = this.value) { 14 | is Array<*> -> { 15 | thisVal as Array 16 | for (i in thisVal.indices step 2) { 17 | if (key == thisVal[i]) { 18 | val old = thisVal[i + 1] 19 | thisVal[i + 1] = value 20 | return old as V 21 | } 22 | } 23 | if (thisVal.size * 2 > ARRAY_THRESHOLD) { 24 | convertToMap() 25 | return (this.value as MutableMap).put(key, value) 26 | } 27 | val newVal = thisVal.copyOf(thisVal.size + 2) 28 | newVal[thisVal.size] = key 29 | newVal[thisVal.size + 1] = value 30 | this.value = newVal 31 | return null 32 | } 33 | is MutableMap<*, *> -> { 34 | thisVal as MutableMap 35 | return thisVal.put(key, value) 36 | } 37 | else -> throw AssertionError() 38 | } 39 | } 40 | 41 | @Suppress("UNCHECKED_CAST") 42 | override fun get(key: K): V? { 43 | return when (val thisVal = value) { 44 | is Array<*> -> { 45 | for (i in thisVal.indices step 2) { 46 | if (thisVal[i] == key) { 47 | return thisVal[i + 1] as V 48 | } 49 | } 50 | null 51 | } 52 | is MutableMap<*, *> -> thisVal[key] as V? 53 | else -> throw AssertionError() 54 | } 55 | } 56 | 57 | override fun containsKey(key: K): Boolean { 58 | return when (val thisVal = value) { 59 | is Array<*> -> { 60 | for (i in thisVal.indices step 2) { 61 | if (thisVal[i] == key) { 62 | return true 63 | } 64 | } 65 | false 66 | } 67 | is MutableMap<*, *> -> thisVal.containsKey(key) 68 | else -> throw AssertionError() 69 | } 70 | } 71 | 72 | override fun containsValue(value: V): Boolean { 73 | return when (val thisVal = value) { 74 | is Array<*> -> { 75 | for (i in thisVal.indices step 2) { 76 | if (thisVal[i + 1] == value) { 77 | return true 78 | } 79 | } 80 | false 81 | } 82 | is MutableMap<*, *> -> thisVal.containsValue(value) 83 | else -> throw AssertionError() 84 | } 85 | } 86 | 87 | @Suppress("UNCHECKED_CAST") 88 | override fun remove(key: K): V? { 89 | return when (val thisVal = value) { 90 | is Array<*> -> { 91 | for (i in thisVal.indices step 2) { 92 | if (thisVal[i] == key) { 93 | val prevVal = thisVal[i + 1] as V 94 | val newArr = thisVal.copyOf(thisVal.size - 2) 95 | System.arraycopy(thisVal, i + 2, newArr, i, thisVal.size - (i + 2)) 96 | value = newArr 97 | return prevVal 98 | } 99 | } 100 | null 101 | } 102 | is MutableMap<*, *> -> { 103 | thisVal as MutableMap 104 | val prevVal = thisVal.remove(key) 105 | if (thisVal.size <= ARRAY_THRESHOLD) { 106 | convertToArray() 107 | } 108 | prevVal 109 | } 110 | else -> throw AssertionError() 111 | } 112 | } 113 | 114 | override fun clear() { 115 | value = emptyArray() 116 | } 117 | 118 | override val size 119 | get() = when (val thisVal = value) { 120 | is Array<*> -> thisVal.size / 2 121 | is MutableMap<*, *> -> thisVal.size 122 | else -> throw AssertionError() 123 | } 124 | 125 | override val entries: MutableSet> 126 | get() = entriesCache 127 | 128 | private val entriesCache by lazy { 129 | object : AbstractMutableSet>() { 130 | private inner class ArrayItr : MutableIterator> { 131 | private var index = 0 132 | private var canRemove = false 133 | private var removed = false 134 | 135 | override fun hasNext() = index < (value as? Array<*> ?: throw ConcurrentModificationException()).size 136 | 137 | @Suppress("UNCHECKED_CAST") 138 | override fun next(): MutableMap.MutableEntry { 139 | val thisVal1 = value as? Array<*> ?: throw ConcurrentModificationException() 140 | if (removed) throw IllegalStateException() 141 | if (index >= thisVal1.size) throw NoSuchElementException() 142 | removed = false 143 | val ret = MutableEntry(thisVal1[index] as K, thisVal1[index + 1] as V, this@SmartMap, index) 144 | index += 2 145 | canRemove = true 146 | return ret 147 | } 148 | 149 | @Suppress("UNCHECKED_CAST") 150 | override fun remove() { 151 | if (!canRemove) throw IllegalStateException() 152 | canRemove = false 153 | removed = true 154 | val thisVal1 = value as? Array ?: throw ConcurrentModificationException() 155 | if (index >= thisVal1.size + 2) throw ConcurrentModificationException() 156 | val newVal = thisVal1.copyOf(thisVal1.size - 2) 157 | System.arraycopy(thisVal1, index, newVal, index - 2, thisVal1.size - index) 158 | index -= 2 159 | value = newVal 160 | } 161 | } 162 | 163 | override fun add(element: MutableMap.MutableEntry): Boolean { 164 | return put(element.key, element.value) == null 165 | } 166 | 167 | @Suppress("UNCHECKED_CAST") 168 | override fun iterator(): MutableIterator> { 169 | return when (val thisVal = value) { 170 | is Array<*> -> ArrayItr() 171 | is MutableMap<*, *> -> { 172 | thisVal as MutableMap 173 | thisVal.entries.iterator() 174 | } 175 | else -> throw AssertionError() 176 | } 177 | } 178 | 179 | override val size = this@SmartMap.size 180 | 181 | override fun contains(element: MutableMap.MutableEntry) = containsKey(element.key) 182 | } 183 | } 184 | 185 | private class MutableEntry( 186 | private val k: K, 187 | v: V, 188 | private val smartMap: SmartMap, 189 | private var index: Int 190 | ) : MutableMap.MutableEntry { 191 | private var cachedValue: V = v 192 | 193 | override val key = k 194 | 195 | @Suppress("UNCHECKED_CAST") 196 | override val value: V 197 | get() { 198 | return when (val thisVal = smartMap.value) { 199 | is Array<*> -> { 200 | if (index < thisVal.size && thisVal[index] == k) { 201 | cachedValue = thisVal[index + 1] as V 202 | return cachedValue 203 | } 204 | for (i in thisVal.indices step 2) { 205 | if (thisVal[i] == k) { 206 | index = i 207 | cachedValue = thisVal[index + 1] as V 208 | return cachedValue 209 | } 210 | } 211 | cachedValue 212 | } 213 | is MutableMap<*, *> -> { 214 | val ret = thisVal[k] as V? 215 | if (ret != null) cachedValue = ret 216 | cachedValue 217 | } 218 | else -> throw AssertionError() 219 | } 220 | } 221 | 222 | @Suppress("UNCHECKED_CAST") 223 | override fun setValue(newValue: V): V { 224 | val defaultOldVal = cachedValue 225 | cachedValue = newValue 226 | return when (val thisVal = smartMap.value) { 227 | is Array<*> -> { 228 | thisVal as Array 229 | if (index < thisVal.size && thisVal[index] == k) { 230 | val oldVal = thisVal[index + 1] as V 231 | thisVal[index + 1] = newValue 232 | return oldVal 233 | } 234 | for (i in thisVal.indices step 2) { 235 | index = i 236 | val oldVal = thisVal[i + 1] as V 237 | thisVal[i + 1] = newValue 238 | return oldVal 239 | } 240 | defaultOldVal 241 | } 242 | is MutableMap<*, *> -> { 243 | thisVal as MutableMap 244 | thisVal.put(k, newValue) ?: defaultOldVal 245 | } 246 | else -> throw AssertionError() 247 | } 248 | } 249 | 250 | override fun equals(other: Any?): Boolean { 251 | val that = other as? Map.Entry<*, *> ?: return false 252 | return key == that.key && value == that.value 253 | } 254 | 255 | override fun hashCode(): Int { 256 | return key.hashCode() xor value.hashCode() 257 | } 258 | 259 | override fun toString(): String { 260 | return "($key, $value)" 261 | } 262 | } 263 | 264 | private fun convertToMap() { 265 | val arr = value as Array<*> 266 | val newVal = hashMapOf() 267 | for (i in arr.indices step 2) { 268 | newVal[arr[i]] = arr[i + 1] 269 | } 270 | value = newVal 271 | } 272 | 273 | private fun convertToArray() { 274 | val map = value as Map<*, *> 275 | val newVal = arrayOfNulls(map.size * 2) 276 | for ((index, entry) in map.entries.withIndex()) { 277 | newVal[index * 2] = entry.key 278 | newVal[index * 2 + 1] = entry.value 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/main/kotlin/net/earthcomputer/classfileindexer/utils.kt: -------------------------------------------------------------------------------- 1 | package net.earthcomputer.classfileindexer 2 | 3 | import com.google.common.collect.MapMaker 4 | import com.intellij.codeEditor.JavaEditorFileSwapper 5 | import com.intellij.openapi.application.ApplicationManager 6 | import com.intellij.openapi.application.runReadAction 7 | import com.intellij.openapi.progress.ProgressManager 8 | import com.intellij.openapi.project.DumbService 9 | import com.intellij.openapi.project.Project 10 | import com.intellij.openapi.vfs.VirtualFile 11 | import com.intellij.psi.JavaElementVisitor 12 | import com.intellij.psi.JavaPsiFacade 13 | import com.intellij.psi.PsiAnonymousClass 14 | import com.intellij.psi.PsiArrayType 15 | import com.intellij.psi.PsiClass 16 | import com.intellij.psi.PsiClassType 17 | import com.intellij.psi.PsiCompiledFile 18 | import com.intellij.psi.PsiElement 19 | import com.intellij.psi.PsiField 20 | import com.intellij.psi.PsiManager 21 | import com.intellij.psi.PsiMethod 22 | import com.intellij.psi.PsiPrimitiveType 23 | import com.intellij.psi.PsiType 24 | import com.intellij.psi.search.GlobalSearchScope 25 | import com.intellij.psi.util.CachedValue 26 | import com.intellij.psi.util.CachedValueProvider 27 | import com.intellij.psi.util.CachedValuesManager 28 | import com.intellij.psi.util.PsiModificationTracker 29 | import com.intellij.psi.util.PsiTreeUtil 30 | import com.intellij.util.SlowOperations 31 | import net.earthcomputer.classfileindexer.libs.org.objectweb.asm.Type 32 | 33 | private val internalNamesCache = MapMaker().weakKeys().makeMap>() // concurrent, uses identity for keys 34 | private fun PsiClass.computeInternalName(): String? { 35 | val containingClass = containingClass ?: return qualifiedName?.replace('.', '/') 36 | val containingInternalName = containingClass.internalName ?: return null 37 | val name = name 38 | if (name != null) { 39 | return "$containingInternalName\$$name" 40 | } 41 | var anonymousClassId = 1 42 | var found = false 43 | containingClass.accept(object : JavaElementVisitor() { 44 | override fun visitAnonymousClass(aClass: PsiAnonymousClass?) { 45 | if (!found) { 46 | if (aClass === this@computeInternalName) { 47 | found = true 48 | } else { 49 | anonymousClassId++ 50 | } 51 | } 52 | } 53 | }) 54 | if (!found) return null 55 | return "$containingInternalName\$$anonymousClassId" 56 | } 57 | 58 | val PsiClass.internalName: String? 59 | get() { 60 | ApplicationManager.getApplication().assertReadAccessAllowed() 61 | return internalNamesCache.computeIfAbsent(this) { 62 | CachedValuesManager.getManager(project).createCachedValue { 63 | CachedValueProvider.Result.create(computeInternalName(), PsiModificationTracker.MODIFICATION_COUNT) 64 | } 65 | }.value 66 | } 67 | 68 | val PsiClass.descriptor: String? 69 | get() = internalName?.let { "L$it;" } 70 | 71 | val PsiType.descriptor: String? 72 | get() { 73 | return when (this) { 74 | is PsiPrimitiveType -> kind.binaryName 75 | is PsiArrayType -> "[".repeat(arrayDimensions) + deepComponentType.descriptor 76 | is PsiClassType -> resolve()?.descriptor 77 | ?: ("L" + canonicalText.replace('.', '/') + ";") // fails inner classes, best we can do 78 | else -> null 79 | } 80 | } 81 | 82 | val PsiField.descriptor: String? 83 | get() = type.descriptor 84 | 85 | val PsiMethod.descriptor: String? 86 | get() { 87 | val descriptors = parameterList.parameters.map { it.type.descriptor } 88 | if (descriptors.contains(null)) return null 89 | val returnDesc = if (isConstructor) { 90 | "V" 91 | } else { 92 | returnType?.descriptor ?: return null 93 | } 94 | return "(" + descriptors.joinToString("") + ")" + returnDesc 95 | } 96 | 97 | inline fun PsiElement.getParentOfType() = PsiTreeUtil.getParentOfType(this, T::class.java) 98 | 99 | fun Type.isPrimitive() = sort != Type.ARRAY && sort != Type.OBJECT && sort != Type.METHOD 100 | 101 | fun isDescriptorOfType(desc: String, type: PsiType) = isDescriptorOfType(Type.getType(desc), type) 102 | 103 | private val SLASHES_AND_DOLLARS = "[/$]".toRegex() 104 | fun isDescriptorOfType(desc: Type, type: PsiType): Boolean { 105 | return when (type) { 106 | is PsiArrayType -> 107 | desc.sort == Type.ARRAY && 108 | desc.dimensions == type.arrayDimensions && 109 | isDescriptorOfType(desc.elementType, type.deepComponentType) 110 | is PsiPrimitiveType -> desc.isPrimitive() && type.kind.binaryName == desc.descriptor 111 | is PsiClassType -> { 112 | if (desc.sort != Type.OBJECT) { 113 | return false 114 | } 115 | val heuristic = desc.internalName.replace(SLASHES_AND_DOLLARS, ".").endsWith(type.className) 116 | if (!heuristic) { 117 | return false 118 | } 119 | val resolved = type.resolve() 120 | if (resolved != null) { 121 | return resolved.internalName == desc.internalName 122 | } 123 | // the best we can do 124 | heuristic 125 | } 126 | else -> false 127 | } 128 | } 129 | 130 | fun isDescriptorOfMethodType(desc: String, method: PsiMethod) = isDescriptorOfMethodType(Type.getType(desc), method) 131 | 132 | fun isDescriptorOfMethodType(desc: Type, method: PsiMethod): Boolean { 133 | if (!method.isConstructor) { 134 | val returnType = method.returnType ?: return false 135 | if (!isDescriptorOfType(desc.returnType, returnType)) { 136 | return false 137 | } 138 | } 139 | val args = desc.argumentTypes 140 | val params = method.parameterList.parameters 141 | if (args.size != params.size) { 142 | return false 143 | } 144 | return args.asSequence().zip(params.asSequence()).all { (arg, param) -> isDescriptorOfType(arg, param.type) } 145 | } 146 | 147 | fun getTypeFromDescriptor(project: Project, scope: GlobalSearchScope, desc: String) = getTypeFromDescriptor(project, scope, Type.getType(desc)) 148 | 149 | fun getTypeFromDescriptor(project: Project, scope: GlobalSearchScope, desc: Type): PsiType? { 150 | if (desc.isPrimitive()) return PsiPrimitiveType.fromJvmTypeDescriptor(desc.descriptor[0]) 151 | return when (desc.sort) { 152 | Type.ARRAY -> { 153 | var type = getTypeFromDescriptor(project, scope, desc.elementType) ?: return null 154 | repeat(desc.dimensions) { 155 | type = type.createArrayType() 156 | } 157 | type 158 | } 159 | Type.OBJECT -> { 160 | val elementFactory = JavaPsiFacade.getElementFactory(project) 161 | val innerClasses = desc.internalName.split("$") 162 | if (innerClasses.size == 1) { 163 | return elementFactory.createTypeByFQClassName(innerClasses[0].replace('/', '.'), scope) 164 | } 165 | var clazz = JavaPsiFacade.getInstance(project).findClass(innerClasses[0].replace('/', '.'), scope) ?: return null 166 | for (innerName in innerClasses.subList(1, innerClasses.size)) { 167 | clazz = clazz.findInnerClassByName(innerName, false) ?: return null 168 | } 169 | elementFactory.createType(clazz) 170 | } 171 | else -> null 172 | } 173 | } 174 | 175 | fun findCompiledFileWithoutSources(project: Project, file: VirtualFile): PsiCompiledFile? { 176 | val className = file.nameWithoutExtension 177 | val actualFile = if (className.contains("\$")) { 178 | file.parent?.findChild("${className.substringBefore("\$")}.class") ?: file 179 | } else { 180 | file 181 | } 182 | if (JavaEditorFileSwapper.findSourceFile(project, actualFile) != null) return null 183 | return PsiManager.getInstance(project).findFile(actualFile) as? PsiCompiledFile 184 | } 185 | 186 | fun runReadActionInSmartModeWithWritePriority(project: Project, validityCheck: () -> Boolean, action: () -> Unit): Boolean { 187 | // avoid deadlocks, IndexNotReadyException may be thrown later 188 | if (ApplicationManager.getApplication().isDispatchThread) { 189 | SlowOperations.assertSlowOperationsAreAllowed() 190 | var result = false 191 | runReadAction { 192 | if (validityCheck()) { 193 | action() 194 | result = true 195 | } 196 | } 197 | return result 198 | } 199 | 200 | val hasReadAccess = ApplicationManager.getApplication().isReadAccessAllowed 201 | 202 | val dumbService = DumbService.getInstance(project) 203 | val progressManager = ProgressManager.getInstance() 204 | 205 | var completed = false 206 | var canceledInvalid = false 207 | while (!completed) { 208 | if (!hasReadAccess) { 209 | dumbService.waitForSmartMode() 210 | } 211 | val canceledByWrite = !progressManager.runInReadActionWithWriteActionPriority( 212 | action@{ 213 | if (!project.isOpen || !validityCheck()) { 214 | canceledInvalid = true 215 | return@action 216 | } 217 | if (!hasReadAccess && dumbService.isDumb) { 218 | return@action 219 | } 220 | action() 221 | completed = true 222 | }, 223 | null 224 | ) 225 | if (canceledInvalid) { 226 | return false 227 | } 228 | if (canceledByWrite && hasReadAccess) { 229 | // can't wait forever on the dispatch thread, it would cause a deadlock 230 | ProgressManager.canceled(ProgressManager.getInstance().progressIndicator) 231 | ProgressManager.checkCanceled() 232 | break 233 | } 234 | } 235 | 236 | return true 237 | } 238 | 239 | @Deprecated("Remove when 2021.1 support is dropped") 240 | private val SLOW_ALLOWED_FLAG = runCatching { 241 | SlowOperations::class.java.getDeclaredField("ourAllowedFlag") 242 | .let { it.isAccessible = true; it } 243 | }.getOrNull() 244 | 245 | fun withSlowOperationsIfNecessary(func: () -> Unit) { 246 | @Suppress("DEPRECATION") 247 | if (ApplicationManager.getApplication().isDispatchThread && SLOW_ALLOWED_FLAG?.getBoolean(null) != true) { 248 | SlowOperations.allowSlowOperations(func) 249 | } else { 250 | func() 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | net.earthcomputer.classfileindexer 3 | Class File Indexer 4 | Earthcomputer 5 | 6 | 7 | 8 | com.intellij.modules.platform 9 | com.intellij.java 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------