├── .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 |
6 |
7 |
8 |
9 |
12 |
15 |
16 |
17 | true
18 | true
19 | false
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.run/Run IDE with Plugin.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
12 |
17 |
18 |
19 | true
20 | true
21 | false
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.run/Run Plugin Tests.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | true
20 | true
21 | false
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.run/Run Plugin Verification.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | true
20 | true
21 | false
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.run/Run Qodana.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
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 | 
4 | [](https://plugins.jetbrains.com/plugin/PLUGIN_ID)
5 | [](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