├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── assets └── img │ ├── gutter.gif │ ├── inspection.gif │ ├── proactive.gif │ ├── recovering.jpeg │ └── settings.png ├── build.gradle.kts ├── evaluation ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── java │ └── org │ │ └── jetbrains │ │ └── research │ │ └── ddtm │ │ └── evaluation │ │ ├── EvaluationRunner.java │ │ └── IntellijProjectUtils.java │ └── resources │ └── META-INF │ └── plugin.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── plugin ├── build.gradle.kts └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── jetbrains │ │ │ └── research │ │ │ └── ddtm │ │ │ ├── Config.java │ │ │ ├── DataDrivenTypeMigrationBundle.java │ │ │ ├── data │ │ │ ├── TypeChangeRulesStorage.java │ │ │ ├── enums │ │ │ │ ├── InvocationWorkflow.java │ │ │ │ └── SupportedSearchScope.java │ │ │ ├── models │ │ │ │ ├── TypeChange.java │ │ │ │ ├── TypeChangePatternDescriptor.java │ │ │ │ └── TypeChangeRuleDescriptor.java │ │ │ └── specifications │ │ │ │ ├── AbstractPatternSpecification.java │ │ │ │ ├── AndSpecification.java │ │ │ │ ├── SourceTypeSpecification.java │ │ │ │ └── TargetTypeSpecification.java │ │ │ ├── ide │ │ │ ├── fus │ │ │ │ ├── TypeChangeLogger.java │ │ │ │ ├── TypeChangeLoggerProvider.java │ │ │ │ └── TypeChangeLogsCollector.java │ │ │ ├── inspections │ │ │ │ ├── TypeChangeInspection.java │ │ │ │ └── TypeChangeQuickFix.java │ │ │ ├── intentions │ │ │ │ ├── FailedTypeChangeRecoveringIntention.java │ │ │ │ ├── ProactiveTypeChangeIntention.java │ │ │ │ └── SuggestedTypeChangeIntention.java │ │ │ ├── migration │ │ │ │ ├── HeuristicTypeConversionDescriptor.java │ │ │ │ ├── HeuristicTypeConversionRule.java │ │ │ │ ├── TypeChangeProcessor.java │ │ │ │ ├── collectors │ │ │ │ │ ├── RequiredImportsCollector.java │ │ │ │ │ ├── SwitchableCollector.java │ │ │ │ │ └── TypeChangesInfoCollector.java │ │ │ │ └── structuralsearch │ │ │ │ │ ├── MyReplacer.java │ │ │ │ │ └── SSRUtils.java │ │ │ ├── refactoring │ │ │ │ ├── ReactiveTypeChangeAvailabilityUpdater.java │ │ │ │ ├── TypeChangeDialog.java │ │ │ │ ├── TypeChangeGutterIconRenderer.java │ │ │ │ ├── TypeChangeMarker.java │ │ │ │ ├── TypeChangeSuggestedRefactoringState.java │ │ │ │ ├── listeners │ │ │ │ │ ├── AfterTypeChangeCaretListener.java │ │ │ │ │ ├── RenameRefactoringEventListener.java │ │ │ │ │ ├── TypeChangeDocumentListener.java │ │ │ │ │ └── UndoTypeChangeListener.java │ │ │ │ └── services │ │ │ │ │ ├── TypeChangeIntentionContributor.java │ │ │ │ │ └── TypeChangeRefactoringProvider.java │ │ │ ├── settings │ │ │ │ ├── TypeChangeSettingsComponent.java │ │ │ │ ├── TypeChangeSettingsConfigurable.java │ │ │ │ └── TypeChangeSettingsState.java │ │ │ └── ui │ │ │ │ ├── FailedTypeChangesPanel.java │ │ │ │ ├── FailedTypeChangesUsagesPanel.java │ │ │ │ ├── TypeChangeClassicModePanel.java │ │ │ │ ├── TypeChangeGutterPopupPanel.java │ │ │ │ └── TypeChangesListPopupStep.java │ │ │ ├── legacy │ │ │ ├── CloningRepositoryTest.java │ │ │ ├── PsiTypeChangeListener.java │ │ │ ├── TypeRelevantUsagesProcessor.java │ │ │ ├── TypeRelevantUsagesVisitor.java │ │ │ └── Utils.java │ │ │ └── utils │ │ │ ├── EditorUtils.java │ │ │ ├── PsiRelatedUtils.java │ │ │ ├── StringUtils.java │ │ │ └── TypeReference.java │ └── resources │ │ ├── DataDrivenTypeMigrationBundle.properties │ │ ├── META-INF │ │ └── plugin.xml │ │ ├── dbp-ddtm-count.json │ │ ├── inspectionDescriptions │ │ └── DDTM.html │ │ ├── intentionDescriptions │ │ └── ProactiveTypeChangeIntention │ │ │ └── description.html │ │ └── rules.json │ └── test │ ├── java │ └── org │ │ └── jetbrains │ │ └── research │ │ └── ddtm │ │ └── ide │ │ └── TypeChangeIntentionTest.java │ └── resources │ ├── mockJDK-1.8 │ └── jre │ │ └── lib │ │ ├── annotations.jar │ │ ├── annotations.jar_LICENSE │ │ ├── rt.jar │ │ └── rt.jar_LICENSE │ └── testData │ ├── TestFileToPath │ ├── Expected.java │ └── Initial.java │ └── TestListToSet │ ├── Expected.java │ └── Initial.java ├── scripts ├── add_ranks.py └── logging_json_prepare.py └── settings.gradle.kts /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | setup: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up JDK 11 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '11' 20 | distribution: 'adopt' 21 | 22 | - name: Validate Gradle wrapper 23 | uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b 24 | 25 | - name: Cache Gradle packages 26 | uses: actions/cache@v2 27 | with: 28 | path: | 29 | ~/.gradle/caches 30 | ~/.gradle/wrapper 31 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 32 | restore-keys: | 33 | ${{ runner.os }}-gradle- 34 | 35 | build: 36 | needs: setup 37 | runs-on: ubuntu-latest 38 | 39 | steps: 40 | - uses: actions/checkout@v2 41 | 42 | - name: Set up JDK 11 43 | uses: actions/setup-java@v2 44 | with: 45 | java-version: '11' 46 | distribution: 'adopt' 47 | 48 | - name: Build with Gradle 49 | run: ./gradlew build -x test 50 | 51 | test: 52 | needs: setup 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - uses: actions/checkout@v2 57 | 58 | - name: Set up JDK 11 59 | id: setup-java-11 60 | uses: actions/setup-java@v2 61 | with: 62 | java-version: '11' 63 | distribution: 'adopt' 64 | 65 | - name: Run tests 66 | run: ./gradlew test -Djdk.home.path="${{ steps.setup-java-11.outputs.path }}" 67 | 68 | - name: Upload Test Report 69 | uses: actions/upload-artifact@v2 70 | if: ${{ always() }} 71 | with: 72 | name: test-report 73 | path: | 74 | plugin/build/reports/tests/**/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # User-specific stuff 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/**/usage.statistics.xml 8 | .idea/**/dictionaries 9 | .idea/**/shelf 10 | 11 | # Generated files 12 | .idea/**/contentModel.xml 13 | 14 | # Sensitive or high-churn files 15 | .idea/**/dataSources/ 16 | .idea/**/dataSources.ids 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | .idea/**/dbnavigator.xml 22 | 23 | # Gradle 24 | .idea/**/gradle.xml 25 | .idea/**/libraries 26 | 27 | # Gradle and Maven with auto-import 28 | # When using Gradle or Maven with auto-import, you should exclude module files, 29 | # since they will be recreated, and may cause churn. Uncomment if using 30 | # auto-import. 31 | # .idea/artifacts 32 | # .idea/compiler.xml 33 | # .idea/jarRepositories.xml 34 | # .idea/modules.xml 35 | # .idea/*.iml 36 | # .idea/modules 37 | # *.iml 38 | # *.ipr 39 | 40 | # CMake 41 | cmake-build-*/ 42 | 43 | # Mongo Explorer plugin 44 | .idea/**/mongoSettings.xml 45 | 46 | # File-based project format 47 | *.iws 48 | 49 | # IntelliJ 50 | out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | # Editor-based Rest Client 68 | .idea/httpRequests 69 | 70 | # Android studio 3.1+ serialized cache file 71 | .idea/caches/build_file_checksums.ser 72 | 73 | .gradle/ 74 | build/ 75 | .idea/ 76 | logging-*.json 77 | *.iml 78 | 79 | scripts/data/TypeChangeSummary.html -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Oleg Smirnov, Ameya Ketkar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Data-Driven Type Migration 2 | 3 | [![JB Research](https://jb.gg/badges/research-flat-square.svg)](https://research.jetbrains.org/) 4 | ![pipeline status](https://github.com/JetBrains-Research/data-driven-type-migration/actions/workflows/build.yml/badge.svg) 5 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://opensource.org/licenses/MIT) 6 | 7 | An IntelliJ IDEA plugin that adapts the current approach of `Type Migration` refactoring for Java to use custom 8 | structural-replace templates that express the adaptations required to perform the type change. We inferred these type 9 | change rules by mining 250 popular open-source Java repositories. In general, it helps to automate the process of 10 | updating the data-flow dependent references of a program element, which type has been changed. 11 | 12 | The plugin is compatible with **IntelliJ IDEA 2021.1** and can be built from sources. 13 | 14 | ## Installation 15 | 16 | At first, clone this repository and open the root folder. 17 | 18 | **Build the plugin from sources and go:** 19 | 20 | - Run `./gradlew :plugin:buildPlugin` 21 | - Check out `./plugin/build/distributions/plugin-*.zip` 22 | - Install the plugin in your **IntelliJ IDEA 2021.1** via `File` - `Settings` - `Plugins` 23 | - `Install Plugin from Disk...` 24 | 25 | **Quick IDE launch for evaluation:** 26 | 27 | - Run `./gradlew :plugin:runIde` 28 | 29 | ## Overview 30 | 31 | There are several types of [Code Intentions](https://plugins.jetbrains.com/docs/intellij/code-intentions.html) 32 | provided by the plugin: 33 | 34 | ### Proactive Type Change Intention 35 | 36 | This is a usual code intention that can be invoked from the `Show Context Actions` 37 | item in the popup menu, when you click on the `Type Element` in your Java code. Then you can choose any of the 38 | suggested `Type Migration Rules` from the dropdown list, select the search scope, and the plugin will try to update the data-flow dependent 39 | references of the selected `Type Element`: 40 | 41 | Proactive Intention Example 42 | 43 | If the plugin does not succeed in migrating some references, it will show the 44 | `Failed Type Changes` tool window. Moreover, for some of them it can suggest another type conversion rules using another 45 | type of code intention — 46 | **Recovering Type Change Intention**. But this one could also change the type of the overall expression or statement in 47 | your code, so be careful when applying them. 48 | 49 | Recovering Intention Example 50 | 51 | ### Reactive Type Change Intention* 52 | 53 | **Experimental feature* 54 | 55 | This intention (and corresponding refactoring) is suggested when the user changes some `Type Element` in the Java code 56 | manually. After the single type change is performed, you can click on the icon that appears on the gutter, and run the type migration from there: 57 | 58 | Gutter Icon Example 59 | 60 | *Note: the reactive intention for the particular type element in the code turns off by timeout (10 sec by default).* 61 | 62 | ### Settings 63 | 64 | You can configure the default `Search Scope` for type migration or `Reactive Intention Disabling Timeout` 65 | in the menu of the plugin: `File` - `Settings` - `Tools` - `Data-Driven Type Migration`. 66 | 67 | Also, you can always edit any of type change patterns or rewrite rules manually in the Settings menu tab: 68 | 69 | Gutter Icon Example 70 | 71 | ### Inspections 72 | 73 | The plugin will also try to recommend you some type changes to apply in the form 74 | of [Code Inspection](https://www.jetbrains.com/help/idea/code-inspection.html). Generally, these suggestions will have 75 | a `WARNING` level and are based on the recommendations from Effective Java, eliminating the misuses of Java 8 76 | functional interfaces and unnecessary boxing. 77 | 78 | Gutter Icon Example 79 | 80 | **Note: the full list of supported type change patterns can be found [here](https://type-change.github.io/patterns.html).** -------------------------------------------------------------------------------- /assets/img/gutter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/data-driven-type-migration/9f0263be53dc34993f9aae9e8f056f1491efda87/assets/img/gutter.gif -------------------------------------------------------------------------------- /assets/img/inspection.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/data-driven-type-migration/9f0263be53dc34993f9aae9e8f056f1491efda87/assets/img/inspection.gif -------------------------------------------------------------------------------- /assets/img/proactive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/data-driven-type-migration/9f0263be53dc34993f9aae9e8f056f1491efda87/assets/img/proactive.gif -------------------------------------------------------------------------------- /assets/img/recovering.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/data-driven-type-migration/9f0263be53dc34993f9aae9e8f056f1491efda87/assets/img/recovering.jpeg -------------------------------------------------------------------------------- /assets/img/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/data-driven-type-migration/9f0263be53dc34993f9aae9e8f056f1491efda87/assets/img/settings.png -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | group = "org.jetbrains.research.ddtm" 2 | version = "1.0-SNAPSHOT" 3 | 4 | plugins { 5 | java 6 | id("org.jetbrains.intellij") version "0.7.2" 7 | } 8 | 9 | allprojects { 10 | repositories { 11 | jcenter() 12 | mavenCentral() 13 | } 14 | } -------------------------------------------------------------------------------- /evaluation/README.md: -------------------------------------------------------------------------------- 1 | ## Evaluation CLI Runner 2 | 3 | - Run the script via: 4 | 5 | ``` 6 | ./gradlew :evaluation:cli -PsourceProjectsPath=path/to/projects/for/evaluation -PjdkPath=path/to/jdk8 7 | ``` 8 | 9 | - Gradle task arguments: 10 | - `-PsourceProjectsPath`: path to the directory with cloned and prepared projects for evaluation. The plugin expects 11 | that at least one `.java` file will contain `` tag on some type element to extract the migrating usages 12 | from it. 13 | - `-PjdkPath`: path to the installed JDK 1.8. Usually, it should be something 14 | like `/usr/lib/jvm/java-8-openjdk-amd64` -------------------------------------------------------------------------------- /evaluation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | group = rootProject.group 2 | version = rootProject.version 3 | 4 | plugins { 5 | java 6 | id("org.jetbrains.intellij") 7 | } 8 | 9 | intellij { 10 | type = "IC" 11 | version = "2021.1" 12 | setPlugins("java") 13 | } 14 | 15 | dependencies { 16 | implementation(project(":plugin")) 17 | compileOnly("commons-cli:commons-cli:1.4") 18 | } 19 | 20 | tasks { 21 | runIde { 22 | val sourceProjectsPath: String? by project 23 | val jdkPath: String? by project 24 | args = listOfNotNull( 25 | "evaluation", 26 | sourceProjectsPath?.let { "--src-projects-dir=$it" }, 27 | jdkPath?.let { "--jdk-path=$it" } 28 | ) 29 | jvmArgs = listOf("-Djava.awt.headless=true") 30 | standardInput = System.`in` 31 | standardOutput = System.`out` 32 | } 33 | 34 | register("cli") { 35 | dependsOn("runIde") 36 | } 37 | } 38 | 39 | tasks.withType() 40 | .forEach { it.enabled = false } -------------------------------------------------------------------------------- /evaluation/src/main/java/org/jetbrains/research/ddtm/evaluation/EvaluationRunner.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.evaluation; 2 | 3 | import com.intellij.ide.impl.ProjectUtil; 4 | import com.intellij.openapi.application.ApplicationManager; 5 | import com.intellij.openapi.application.ApplicationStarter; 6 | import com.intellij.openapi.command.WriteCommandAction; 7 | import com.intellij.openapi.diagnostic.Logger; 8 | import com.intellij.openapi.project.Project; 9 | import com.intellij.openapi.roots.ProjectRootManager; 10 | import com.intellij.openapi.vfs.VfsUtilCore; 11 | import com.intellij.openapi.vfs.VirtualFile; 12 | import com.intellij.psi.*; 13 | import com.intellij.usageView.UsageInfo; 14 | import org.apache.commons.cli.*; 15 | import org.jetbrains.annotations.NonNls; 16 | import org.jetbrains.annotations.NotNull; 17 | import org.jetbrains.research.ddtm.data.TypeChangeRulesStorage; 18 | import org.jetbrains.research.ddtm.data.enums.InvocationWorkflow; 19 | import org.jetbrains.research.ddtm.data.enums.SupportedSearchScope; 20 | import org.jetbrains.research.ddtm.ide.migration.TypeChangeProcessor; 21 | import org.jetbrains.research.ddtm.ide.migration.collectors.RequiredImportsCollector; 22 | import org.jetbrains.research.ddtm.ide.migration.collectors.TypeChangesInfoCollector; 23 | import org.jetbrains.research.ddtm.ide.settings.TypeChangeSettingsState; 24 | import org.jetbrains.research.ddtm.utils.PsiRelatedUtils; 25 | 26 | import java.io.File; 27 | import java.util.Arrays; 28 | import java.util.List; 29 | import java.util.Objects; 30 | 31 | public class EvaluationRunner implements ApplicationStarter { 32 | private static final Logger LOG = Logger.getInstance(EvaluationRunner.class); 33 | private static final String JDK_NAME = "jdk"; 34 | 35 | @Override 36 | public @NonNls 37 | String getCommandName() { 38 | return "evaluation"; 39 | } 40 | 41 | 42 | @Override 43 | public void main(@NotNull List args) { 44 | try { 45 | CommandLine parsedArgs = parseArgs(args.toArray(new String[0])); 46 | File pathToProjects = new File(parsedArgs.getOptionValue("src-projects-dir")); 47 | String pathToJdk = parsedArgs.getOptionValue("jdk-path"); 48 | 49 | ApplicationManager.getApplication().runWriteAction(() -> { 50 | 51 | // Traverse all subdirectories in the specified directory and create project for each of them 52 | for (File projectDir : Objects.requireNonNull(pathToProjects.listFiles())) { 53 | if (projectDir.isDirectory()) { 54 | final Project project = ProjectUtil.openOrImport(projectDir.toPath(), null, false); 55 | if (project == null) continue; 56 | 57 | IntellijProjectUtils.loadModules(projectDir, project); 58 | IntellijProjectUtils.setupJdk(project, pathToJdk); 59 | 60 | TypeChangeSettingsState.getInstance().searchScope = SupportedSearchScope.PROJECT; 61 | 62 | // Just for test: the types for Type Change should be specified / injected from outside 63 | final String sourceType = "java.io.File"; 64 | final String targetType = "java.nio.file.Path"; 65 | 66 | // Traverse all the `.java` files in the project and build PSI for each of them 67 | VirtualFile[] roots = ProjectRootManager.getInstance(project).getContentRoots(); 68 | for (var root : roots) { 69 | VfsUtilCore.iterateChildrenRecursively(root, null, fileOrDir -> { 70 | if (fileOrDir.getExtension() == null 71 | || fileOrDir.getCanonicalPath() == null 72 | || !fileOrDir.getExtension().equals("java")) { 73 | return true; 74 | } 75 | final var psi = PsiManager.getInstance(project).findFile(fileOrDir); 76 | if (psi == null) return true; 77 | 78 | // Find the first `` in the program and repair the corresponding element: 79 | // Like `File` to `File` 80 | // TODO: check the next tags as well 81 | String source = psi.getText(); 82 | int caretOffset = source.indexOf(""); 83 | if (caretOffset == -1) return true; 84 | repairElementWithCaretTag(project, sourceType, psi, caretOffset); 85 | 86 | WriteCommandAction.runWriteCommandAction(project, () -> { 87 | // Create built-in `TypeMigrationProcessor` 88 | final PsiElement context = psi.findElementAt(caretOffset); 89 | final var storage = project.getService(TypeChangeRulesStorage.class); 90 | final var descriptor = storage.findPattern(sourceType, targetType).get(); 91 | final var typeChangeProcessor = new TypeChangeProcessor(project, InvocationWorkflow.PROACTIVE); 92 | final var builtInProcessor = typeChangeProcessor.createBuiltInTypeMigrationProcessor(context, descriptor); 93 | if (builtInProcessor == null) return; 94 | 95 | // Disable plugin's internal usages collectors and find migrating usages in the all project 96 | final var typeChangesCollector = TypeChangesInfoCollector.getInstance(); 97 | final var requiredImportsCollector = RequiredImportsCollector.getInstance(); 98 | typeChangesCollector.off(); 99 | requiredImportsCollector.off(); 100 | final UsageInfo[] usages = builtInProcessor.findUsages(); 101 | 102 | // Traverse the usages, extract PsiElements and parents 103 | for (var usage : usages) { 104 | PsiElement element = usage.getElement(); 105 | assert element != null; 106 | 107 | PsiElement[] parents = { 108 | element.getParent(), 109 | element.getParent().getParent(), 110 | element.getParent().getParent() 111 | }; 112 | System.out.print(element + " - "); 113 | System.out.println(Arrays.toString(parents)); 114 | } 115 | }); 116 | return false; 117 | }); 118 | } 119 | } 120 | } 121 | }); 122 | } catch (Throwable e) { 123 | System.out.println(e.getMessage()); 124 | System.exit(1); 125 | } finally { 126 | System.exit(0); 127 | } 128 | } 129 | 130 | private void repairElementWithCaretTag(Project project, String sourceType, PsiFile psi, int caretOffset) { 131 | PsiElement targetElement = psi.findElementAt(caretOffset); 132 | for (int i = 0; i < 4; ++i) { 133 | targetElement = Objects.requireNonNull(targetElement).getParent(); 134 | } 135 | PsiTypeCodeFragment sourcePsiType = JavaCodeFragmentFactory.getInstance(project) 136 | .createTypeCodeFragment(sourceType, targetElement, true); 137 | PsiTypeElement sourceTypeElement = JavaPsiFacade.getElementFactory(project) 138 | .createTypeElement(Objects.requireNonNull(PsiRelatedUtils.getTypeOfCodeFragment(sourcePsiType))); 139 | PsiElement finalTargetElement = targetElement; 140 | WriteCommandAction.runWriteCommandAction(project, () -> { 141 | finalTargetElement.replace(sourceTypeElement); 142 | }); 143 | } 144 | 145 | private CommandLine parseArgs(String[] args) { 146 | Options options = new Options(); 147 | 148 | Option src = new Option( 149 | "s", 150 | "src-projects-dir", 151 | true, 152 | "path to the directory with projects for evaluation" 153 | ); 154 | src.setRequired(true); 155 | options.addOption(src); 156 | 157 | Option jdk = new Option( 158 | "j", 159 | "jdk-path", 160 | true, 161 | "path to the JDK (such as /usr/lib/jvm/java-8-openjdk-amd64)" 162 | ); 163 | jdk.setRequired(true); 164 | options.addOption(jdk); 165 | 166 | CommandLineParser parser = new DefaultParser(); 167 | HelpFormatter formatter = new HelpFormatter(); 168 | CommandLine cmd = null; 169 | 170 | try { 171 | cmd = parser.parse(options, args); 172 | } catch (ParseException e) { 173 | System.out.println(e.getMessage()); 174 | formatter.printHelp("evaluation", options); 175 | System.exit(1); 176 | } 177 | return cmd; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /evaluation/src/main/java/org/jetbrains/research/ddtm/evaluation/IntellijProjectUtils.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.evaluation; 2 | 3 | import com.intellij.ide.impl.NewProjectUtil; 4 | import com.intellij.openapi.module.JavaModuleType; 5 | import com.intellij.openapi.module.Module; 6 | import com.intellij.openapi.module.ModuleManager; 7 | import com.intellij.openapi.project.Project; 8 | import com.intellij.openapi.projectRoots.JavaSdk; 9 | import com.intellij.openapi.projectRoots.ProjectJdkTable; 10 | import com.intellij.openapi.projectRoots.Sdk; 11 | import com.intellij.openapi.roots.ModuleRootModificationUtil; 12 | import com.intellij.openapi.roots.ProjectRootManager; 13 | import com.intellij.openapi.vfs.LocalFileSystem; 14 | import com.intellij.testFramework.PsiTestUtil; 15 | 16 | import java.io.File; 17 | import java.nio.file.Path; 18 | import java.util.Objects; 19 | 20 | public class IntellijProjectUtils { 21 | public static void setupJdk(Project project, String jdkHomeDirectory) { 22 | Sdk jdk = JavaSdk.getInstance().createJdk(Path.of(jdkHomeDirectory).getFileName().toString(), jdkHomeDirectory, false); 23 | ProjectJdkTable.getInstance().addJdk(jdk); 24 | ProjectRootManager.getInstance(project).setProjectSdk(jdk); 25 | NewProjectUtil.applyJdkToProject(project, jdk); 26 | Module[] modules = ModuleManager.getInstance(project).getModules(); 27 | for (Module module : modules) { 28 | ModuleRootModificationUtil.setModuleSdk(module, jdk); 29 | } 30 | } 31 | 32 | public static void loadModules(File projectDir, Project project) { 33 | // FIXME: there is only one root module in the project (`src` folder) for current test project 34 | // it should resolve any number of modules potentially 35 | final var moduleType = JavaModuleType.getModuleType(); 36 | Path moduleSrcPath = projectDir.toPath().resolve("src"); 37 | Path moduleImlPath = projectDir.toPath().resolve(".iml"); 38 | Module module = ModuleManager.getInstance(project).newModule(moduleImlPath, moduleType.getId()); 39 | PsiTestUtil.addSourceContentToRoots( 40 | module, 41 | Objects.requireNonNull(LocalFileSystem.getInstance().findFileByPath(moduleSrcPath.toString())), 42 | false 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /evaluation/src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | org.jetbrains.research.java-types-migration.evaluation 3 | Evaluation 4 | JetBrains Research 5 | 6 | Type migration plugin evaluation in headless environment 7 | 8 | 10 | com.intellij.modules.platform 11 | com.intellij.modules.java 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/data-driven-type-migration/9f0263be53dc34993f9aae9e8f056f1491efda87/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-6.7-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 | -------------------------------------------------------------------------------- /plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | group = rootProject.group 2 | version = rootProject.group 3 | 4 | plugins { 5 | java 6 | id("org.jetbrains.intellij") 7 | } 8 | 9 | intellij { 10 | type = "IC" 11 | version = "2021.2" 12 | setPlugins("java", "git4idea") 13 | intellij.updateSinceUntilBuild = false 14 | } 15 | 16 | dependencies { 17 | implementation("com.google.code.gson", "gson", "2.8.6") 18 | } 19 | 20 | tasks.withType() 21 | .forEach { it.enabled = false } 22 | 23 | tasks { 24 | test { 25 | systemProperty("jdk.home.path", System.getProperty("jdk.home.path")) 26 | } 27 | } -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/Config.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm; 2 | 3 | public class Config { 4 | public static final int MAX_PARENTS_TO_LIFT_UP = 3; 5 | public static final int DISABLE_INTENTION_TIMEOUT_BY_DEFAULT = 10000; 6 | public static final long GARBAGE_COLLECTOR_FACTOR = 4L; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/DataDrivenTypeMigrationBundle.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm; 2 | 3 | import com.google.common.collect.BiMap; 4 | import com.google.common.collect.HashBiMap; 5 | import com.intellij.AbstractBundle; 6 | import com.intellij.reference.SoftReference; 7 | import org.jetbrains.annotations.PropertyKey; 8 | import org.jetbrains.research.ddtm.data.enums.SupportedSearchScope; 9 | 10 | import java.lang.ref.Reference; 11 | import java.util.Map; 12 | import java.util.ResourceBundle; 13 | 14 | public class DataDrivenTypeMigrationBundle { 15 | public static final BiMap SEARCH_SCOPE_OPTIONS = HashBiMap.create(Map.of( 16 | SupportedSearchScope.LOCAL, message("settings.scope.local"), 17 | SupportedSearchScope.FILE, message("settings.scope.file"), 18 | SupportedSearchScope.PROJECT, message("settings.scope.project") 19 | )); 20 | private static final String BUNDLE = "DataDrivenTypeMigrationBundle"; 21 | private static Reference INSTANCE; 22 | 23 | private DataDrivenTypeMigrationBundle() { 24 | } 25 | 26 | public static String message(@PropertyKey(resourceBundle = BUNDLE) String key, Object... params) { 27 | return AbstractBundle.message(getBundle(), key, params); 28 | } 29 | 30 | private static ResourceBundle getBundle() { 31 | ResourceBundle bundle = SoftReference.dereference(INSTANCE); 32 | if (bundle == null) { 33 | bundle = ResourceBundle.getBundle(BUNDLE); 34 | INSTANCE = new SoftReference<>(bundle); 35 | } 36 | return bundle; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/data/TypeChangeRulesStorage.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.data; 2 | 3 | import com.google.common.reflect.TypeToken; 4 | import com.google.gson.Gson; 5 | import com.intellij.openapi.components.Service; 6 | import com.intellij.openapi.diagnostic.Logger; 7 | import com.intellij.openapi.project.Project; 8 | import org.jetbrains.research.ddtm.data.models.TypeChangePatternDescriptor; 9 | import org.jetbrains.research.ddtm.data.models.TypeChangeRuleDescriptor; 10 | import org.jetbrains.research.ddtm.data.specifications.SourceTypeSpecification; 11 | import org.jetbrains.research.ddtm.data.specifications.TargetTypeSpecification; 12 | import org.jetbrains.research.ddtm.utils.PsiRelatedUtils; 13 | 14 | import java.io.BufferedReader; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.io.InputStreamReader; 18 | import java.lang.reflect.Type; 19 | import java.util.*; 20 | import java.util.stream.Collectors; 21 | 22 | @Service 23 | public final class TypeChangeRulesStorage { 24 | private static final Logger LOG = Logger.getInstance(TypeChangeRulesStorage.class); 25 | 26 | private final Project project; 27 | private final Set sourceTypesCache = new HashSet<>(); 28 | private final Set targetTypesCache = new HashSet<>(); 29 | private List patterns; 30 | private List inspectionPatterns; 31 | 32 | public TypeChangeRulesStorage(Project project) { 33 | this.project = project; 34 | String json; 35 | try { 36 | json = getResourceFileAsString("/rules.json"); 37 | Gson gson = new Gson(); 38 | Type type = new TypeToken>() { 39 | }.getType(); 40 | 41 | this.patterns = gson.fromJson(json, type); 42 | this.inspectionPatterns = Objects.requireNonNull(patterns).stream() 43 | .filter(TypeChangePatternDescriptor::shouldInspect) 44 | .collect(Collectors.toList()); 45 | initCache(); 46 | } catch (IOException e) { 47 | LOG.error(e); 48 | } 49 | } 50 | 51 | private void initCache() { 52 | for (var pattern : patterns) { 53 | sourceTypesCache.add(pattern.getSourceType()); 54 | targetTypesCache.add(pattern.getTargetType()); 55 | } 56 | } 57 | 58 | public Boolean hasSourceType(String sourceType) { 59 | if (sourceTypesCache.contains(sourceType)) return true; 60 | return !getPatternsBySourceType(sourceType).isEmpty(); 61 | } 62 | 63 | public Boolean hasTargetType(String targetType) { 64 | if (targetTypesCache.contains(targetType)) return true; 65 | return !getPatternsByTargetType(targetType).isEmpty(); 66 | } 67 | 68 | public List getPatterns() { 69 | return patterns; 70 | } 71 | 72 | public List getInspectionPatterns() { 73 | return inspectionPatterns; 74 | } 75 | 76 | public List getPatternsBySourceType(String sourceType) { 77 | return patterns.stream() 78 | .filter(new SourceTypeSpecification(sourceType, project)) 79 | .collect(Collectors.toList()); 80 | } 81 | 82 | public Optional findPatternByRule(TypeChangeRuleDescriptor rule) { 83 | return patterns.stream() 84 | .filter(pattern -> pattern.getRules().contains(rule)) 85 | .findFirst(); 86 | } 87 | 88 | public List getPatternsByTargetType(String targetType) { 89 | return patterns.stream() 90 | .filter(new TargetTypeSpecification(targetType, project)) 91 | .collect(Collectors.toList()); 92 | } 93 | 94 | public Optional findPattern(String sourceType, String targetType) { 95 | return patterns.stream() 96 | .filter(new SourceTypeSpecification(sourceType, project) 97 | .and(new TargetTypeSpecification(targetType, project))) 98 | .max(Comparator.comparing(pattern -> PsiRelatedUtils.splitByTokens(pattern.toString()).length)); 99 | } 100 | 101 | private String getResourceFileAsString(String fileName) throws IOException { 102 | try (InputStream stream = TypeChangeRulesStorage.class.getResourceAsStream(fileName)) { 103 | if (stream == null) return null; 104 | try (InputStreamReader isr = new InputStreamReader(stream); 105 | BufferedReader reader = new BufferedReader(isr)) { 106 | return reader.lines().collect(Collectors.joining(System.lineSeparator())); 107 | } 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/data/enums/InvocationWorkflow.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.data.enums; 2 | 3 | public enum InvocationWorkflow { 4 | PROACTIVE, REACTIVE, INSPECTING 5 | } 6 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/data/enums/SupportedSearchScope.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.data.enums; 2 | 3 | public enum SupportedSearchScope { 4 | LOCAL, FILE, PROJECT 5 | } 6 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/data/models/TypeChange.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.data.models; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class TypeChange { 6 | @SerializedName("Before") 7 | private String sourceType; 8 | 9 | @SerializedName("After") 10 | private String targetType; 11 | 12 | public String getSourceType() { 13 | return sourceType; 14 | } 15 | 16 | public String getTargetType() { 17 | return targetType; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/data/models/TypeChangePatternDescriptor.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.data.models; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.psi.PsiType; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.research.ddtm.ide.intentions.SuggestedTypeChangeIntention; 8 | import org.jetbrains.research.ddtm.ide.migration.structuralsearch.SSRUtils; 9 | 10 | import java.util.List; 11 | 12 | public class TypeChangePatternDescriptor { 13 | @SerializedName("ID") 14 | private int id; 15 | 16 | @SerializedName("From") 17 | private String sourceType; 18 | 19 | @SerializedName("To") 20 | private String targetType; 21 | 22 | @SerializedName("Inspect") 23 | private boolean inspect; 24 | 25 | @SerializedName("Kind") 26 | private String kind; 27 | 28 | @SerializedName("Rank") 29 | private Integer rank; 30 | 31 | @SerializedName("Rules") 32 | private List rules; 33 | 34 | public boolean shouldInspect() { 35 | return inspect; 36 | } 37 | 38 | public int getId() { 39 | return id; 40 | } 41 | 42 | public Integer getRank() { 43 | return (rank == null) ? 0 : rank; 44 | } 45 | 46 | public String getTargetType() { 47 | return targetType; 48 | } 49 | 50 | public String getSourceType() { 51 | return sourceType; 52 | } 53 | 54 | public List getRules() { 55 | return rules; 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return sourceType + " to " + targetType; 61 | } 62 | 63 | /** 64 | * Actually, for simple types like File and Path we already have {@link TypeChangePatternDescriptor#getTargetType()}. 65 | * But the following method supposed to resolve more complicated situations including generics, 66 | * like in the pattern "from List<$1$> to Set<$1$>", where we should substitute resolved source type to $1$. 67 | */ 68 | public @NotNull String resolveTargetType(PsiType resolvedSourceType, Project project) { 69 | if (targetType.contains("$")) { 70 | return SSRUtils.substituteTypeByPattern(resolvedSourceType, sourceType, targetType, project); 71 | } 72 | return targetType; 73 | } 74 | 75 | /** 76 | * This method is used for recovering original root type when applying {@link SuggestedTypeChangeIntention} 77 | */ 78 | public String resolveSourceType(PsiType resolvedTargetType, Project project) { 79 | if (sourceType.contains("$")) { 80 | return SSRUtils.substituteTypeByPattern(resolvedTargetType, targetType, sourceType, project); 81 | } 82 | return sourceType; 83 | } 84 | } 85 | 86 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/data/models/TypeChangeRuleDescriptor.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.data.models; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class TypeChangeRuleDescriptor { 6 | @SerializedName("Before") 7 | private String expressionBefore; 8 | 9 | @SerializedName("After") 10 | private String expressionAfter; 11 | 12 | @SerializedName("RequiredImports") 13 | private String requiredImports; 14 | 15 | @SerializedName("ReturnType") 16 | private TypeChange returnType; 17 | 18 | public String getExpressionAfter() { 19 | return expressionAfter; 20 | } 21 | 22 | public String getExpressionBefore() { 23 | return expressionBefore; 24 | } 25 | 26 | public TypeChange getReturnType() { 27 | return returnType; 28 | } 29 | 30 | public String getRequiredImports() { 31 | return requiredImports; 32 | } 33 | 34 | @Override 35 | public String toString() { 36 | return expressionBefore.replaceAll("\\$.*?\\$", "") 37 | + " -> " 38 | + expressionAfter.replaceAll("\\$.*?\\$", ""); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/data/specifications/AbstractPatternSpecification.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.data.specifications; 2 | 3 | import java.util.function.Predicate; 4 | 5 | public abstract class AbstractPatternSpecification implements Predicate { 6 | public AbstractPatternSpecification and(AbstractPatternSpecification other) { 7 | return new AndSpecification<>(this, other); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/data/specifications/AndSpecification.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.data.specifications; 2 | 3 | import java.util.List; 4 | 5 | public class AndSpecification extends AbstractPatternSpecification { 6 | private final List> leafComponents; 7 | 8 | @SafeVarargs 9 | AndSpecification(AbstractPatternSpecification... selectors) { 10 | this.leafComponents = List.of(selectors); 11 | } 12 | 13 | @Override 14 | public boolean test(T t) { 15 | return leafComponents.stream().allMatch(comp -> (comp.test(t))); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/data/specifications/SourceTypeSpecification.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.data.specifications; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import org.jetbrains.research.ddtm.data.models.TypeChangePatternDescriptor; 5 | import org.jetbrains.research.ddtm.ide.migration.structuralsearch.SSRUtils; 6 | 7 | public class SourceTypeSpecification extends AbstractPatternSpecification { 8 | private final Project project; 9 | private final String sourceType; 10 | 11 | public SourceTypeSpecification(String sourceType, Project project) { 12 | this.project = project; 13 | this.sourceType = sourceType; 14 | } 15 | 16 | @Override 17 | public boolean test(TypeChangePatternDescriptor pattern) { 18 | return SSRUtils.hasMatch(sourceType, pattern.getSourceType(), project); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/data/specifications/TargetTypeSpecification.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.data.specifications; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import org.jetbrains.research.ddtm.data.models.TypeChangePatternDescriptor; 5 | import org.jetbrains.research.ddtm.ide.migration.structuralsearch.SSRUtils; 6 | 7 | public class TargetTypeSpecification extends AbstractPatternSpecification { 8 | private final Project project; 9 | private final String targetType; 10 | 11 | public TargetTypeSpecification(String targetType, Project project) { 12 | this.project = project; 13 | this.targetType = targetType; 14 | } 15 | 16 | @Override 17 | public boolean test(TypeChangePatternDescriptor pattern) { 18 | return SSRUtils.hasMatch(targetType, pattern.getTargetType(), project); 19 | } 20 | } -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/fus/TypeChangeLogger.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.fus; 2 | 3 | import com.intellij.internal.statistic.eventLog.EventLogGroup; 4 | import com.intellij.internal.statistic.eventLog.FeatureUsageData; 5 | import com.intellij.internal.statistic.eventLog.StatisticsEventLoggerKt; 6 | import com.intellij.internal.statistic.eventLog.StatisticsEventLoggerProvider; 7 | 8 | public class TypeChangeLogger { 9 | static final StatisticsEventLoggerProvider loggerProvider = 10 | StatisticsEventLoggerKt.getEventLogProvider("DBP"); 11 | 12 | static public final Integer version = loggerProvider.getVersion(); 13 | 14 | static public void log(EventLogGroup group, String action) { 15 | loggerProvider.getLogger().logAsync(group, action, false); 16 | } 17 | 18 | static public void log(EventLogGroup group, String action, FeatureUsageData data) { 19 | loggerProvider.getLogger().logAsync(group, action, data.build(), false); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/fus/TypeChangeLoggerProvider.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.fus; 2 | 3 | import com.intellij.internal.statistic.eventLog.StatisticsEventLoggerProvider; 4 | import com.intellij.internal.statistic.utils.StatisticsUploadAssistant; 5 | import com.intellij.openapi.application.ApplicationManager; 6 | 7 | import java.util.concurrent.TimeUnit; 8 | 9 | public class TypeChangeLoggerProvider extends StatisticsEventLoggerProvider { 10 | public TypeChangeLoggerProvider() { 11 | super("DBP", 1, TimeUnit.MINUTES.toMillis(5), "50KB"); 12 | } 13 | 14 | @Override 15 | public boolean isRecordEnabled() { 16 | return !ApplicationManager.getApplication().isUnitTestMode() && 17 | !ApplicationManager.getApplication().isHeadlessEnvironment() && 18 | StatisticsUploadAssistant.isCollectAllowed(); 19 | } 20 | 21 | @Override 22 | public boolean isSendEnabled() { 23 | return isRecordEnabled() && StatisticsUploadAssistant.isSendAllowed(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/fus/TypeChangeLogsCollector.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.fus; 2 | 3 | import com.intellij.concurrency.JobScheduler; 4 | import com.intellij.internal.statistic.eventLog.EventLogGroup; 5 | import com.intellij.internal.statistic.eventLog.FeatureUsageData; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.psi.PsiElement; 8 | import org.jetbrains.research.ddtm.data.enums.InvocationWorkflow; 9 | 10 | import java.util.concurrent.TimeUnit; 11 | 12 | @SuppressWarnings("UnstableApiUsage") 13 | public class TypeChangeLogsCollector { 14 | private static final Integer LOG_DELAY_MIN = 24 * 60; 15 | private static final Integer LOG_INITIAL_DELAY_MIN = 5; 16 | private static final EventLogGroup group = new EventLogGroup("dbp.ddtm.count", TypeChangeLogger.version); 17 | private static TypeChangeLogsCollector instance; 18 | 19 | private TypeChangeLogsCollector() { 20 | JobScheduler.getScheduler().scheduleWithFixedDelay( 21 | TypeChangeLogsCollector::trackRegistered, 22 | LOG_INITIAL_DELAY_MIN.longValue(), 23 | LOG_DELAY_MIN.longValue(), 24 | TimeUnit.MINUTES 25 | ); 26 | } 27 | 28 | public static TypeChangeLogsCollector getInstance() { 29 | if (instance == null) { 30 | instance = new TypeChangeLogsCollector(); 31 | } 32 | return instance; 33 | } 34 | 35 | private static void trackRegistered() { 36 | TypeChangeLogger.log(group, "registered"); 37 | } 38 | 39 | public void migrationUndone(Project project, int typeChangeId) { 40 | FeatureUsageData data = new FeatureUsageData().addProject(project) 41 | .addData("type_change_id", typeChangeId); 42 | TypeChangeLogger.log(group, "migration.undone", data); 43 | } 44 | 45 | public void renamePerformed(Project project, String elementCanonicalName) { 46 | FeatureUsageData data = new FeatureUsageData().addProject(project) 47 | .addData("element_canonical_name", elementCanonicalName); 48 | TypeChangeLogger.log(group, "rename.performed", data); 49 | } 50 | 51 | public void refactoringIntentionApplied(Project project, int typeChangeId, PsiElement root, int uniqueRulesUsed, 52 | int usagesUpdated, int suspiciousUsagesFound, int usagesFailed, 53 | InvocationWorkflow workflow) { 54 | FeatureUsageData data = new FeatureUsageData().addProject(project) 55 | .addData("type_change_id", typeChangeId) 56 | .addData("migration_root", root.getClass().getName()) 57 | .addData("unique_rules_used", uniqueRulesUsed) 58 | .addData("usages_updated", usagesUpdated) 59 | .addData("suspicious_usages_found", suspiciousUsagesFound) 60 | .addData("usages_failed", usagesFailed - suspiciousUsagesFound) // because every suspicious is also failed 61 | .addData("invocation_workflow", workflow.name().toLowerCase()); 62 | TypeChangeLogger.log(group, "refactoring.intention.applied", data); 63 | } 64 | 65 | public void gutterIconClicked() { 66 | TypeChangeLogger.log(group, "gutter.icon.clicked"); 67 | } 68 | 69 | public void recoveringIntentionApplied(Project project, int typeChangeId) { 70 | FeatureUsageData data = new FeatureUsageData().addProject(project) 71 | .addData("type_change_id", typeChangeId); 72 | TypeChangeLogger.log(group, "recovering.intention.applied", data); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/inspections/TypeChangeInspection.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.inspections; 2 | 3 | import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool; 4 | import com.intellij.codeInspection.ProblemsHolder; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.psi.*; 7 | import com.intellij.psi.util.PsiTreeUtil; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 10 | import org.jetbrains.research.ddtm.data.TypeChangeRulesStorage; 11 | import org.jetbrains.research.ddtm.data.models.TypeChangePatternDescriptor; 12 | import org.jetbrains.research.ddtm.utils.StringUtils; 13 | 14 | import java.nio.file.Path; 15 | import java.util.List; 16 | import java.util.NoSuchElementException; 17 | import java.util.stream.Collectors; 18 | 19 | public class TypeChangeInspection extends AbstractBaseJavaLocalInspectionTool { 20 | @Override 21 | public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) { 22 | return new JavaElementVisitor() { 23 | @Override 24 | public void visitTypeElement(PsiTypeElement type) { 25 | super.visitTypeElement(type); 26 | final Project project = holder.getProject(); 27 | final TypeChangeRulesStorage storage = project.getService(TypeChangeRulesStorage.class); 28 | final String sourceType = type.getType().getCanonicalText(); 29 | if (sourceType.isEmpty() || sourceType.isBlank()) return; 30 | final List inspectionPatterns = storage.getPatternsBySourceType(sourceType).stream() 31 | .filter(TypeChangePatternDescriptor::shouldInspect) 32 | .collect(Collectors.toList()); 33 | if (!inspectionPatterns.isEmpty()) { 34 | holder.registerProblem( 35 | type, 36 | DataDrivenTypeMigrationBundle.message("inspection.problem.descriptor", sourceType), 37 | new TypeChangeQuickFix( 38 | inspectionPatterns, 39 | DataDrivenTypeMigrationBundle.message("inspection.simple.family.name") 40 | ) 41 | ); 42 | } 43 | } 44 | 45 | @Override 46 | public void visitDeclarationStatement(PsiDeclarationStatement statement) { 47 | super.visitDeclarationStatement(statement); 48 | if (statement.getDeclaredElements().length != 1) return; 49 | PsiElement decl = statement.getDeclaredElements()[0]; 50 | 51 | PsiTypeElement sourceTypeElement = PsiTreeUtil.getChildOfType(decl, PsiTypeElement.class); 52 | if (sourceTypeElement == null) return; 53 | 54 | String sourceType = sourceTypeElement.getType().getCanonicalText(); 55 | if (sourceType.equals(String.class.getCanonicalName())) { 56 | PsiLiteral literal = PsiTreeUtil.getChildOfType(decl, PsiLiteral.class); 57 | if (literal == null) return; 58 | 59 | String value = (String) literal.getValue(); 60 | if (StringUtils.isSystemPath(value)) { 61 | final Project project = holder.getProject(); 62 | final TypeChangeRulesStorage storage = project.getService(TypeChangeRulesStorage.class); 63 | final var pattern = storage.findPattern( 64 | String.class.getCanonicalName(), 65 | Path.class.getCanonicalName() 66 | ).orElseThrow(NoSuchElementException::new); 67 | 68 | holder.registerProblem( 69 | sourceTypeElement, 70 | DataDrivenTypeMigrationBundle.message("inspection.problem.descriptor", sourceType), 71 | new TypeChangeQuickFix( 72 | List.of(pattern), 73 | DataDrivenTypeMigrationBundle.message("inspection.smart.string.to.path") 74 | ) 75 | ); 76 | } 77 | } 78 | } 79 | }; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/inspections/TypeChangeQuickFix.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.inspections; 2 | 3 | import com.intellij.codeInspection.LocalQuickFix; 4 | import com.intellij.codeInspection.ProblemDescriptor; 5 | import com.intellij.codeInspection.util.IntentionFamilyName; 6 | import com.intellij.openapi.application.ApplicationManager; 7 | import com.intellij.openapi.project.Project; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.research.ddtm.data.enums.InvocationWorkflow; 10 | import org.jetbrains.research.ddtm.data.models.TypeChangePatternDescriptor; 11 | import org.jetbrains.research.ddtm.ide.refactoring.TypeChangeDialog; 12 | 13 | import java.util.List; 14 | 15 | public class TypeChangeQuickFix implements LocalQuickFix { 16 | List inspectionPatterns; 17 | private final String familyName; 18 | 19 | public TypeChangeQuickFix(List inspectionPatterns, String familyName) { 20 | this.inspectionPatterns = inspectionPatterns; 21 | this.familyName = familyName; 22 | } 23 | 24 | @Override 25 | public @IntentionFamilyName @NotNull String getFamilyName() { 26 | return familyName; 27 | } 28 | 29 | @Override 30 | public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { 31 | ApplicationManager.getApplication().invokeLater(() -> { 32 | final var dialog = new TypeChangeDialog( 33 | inspectionPatterns, InvocationWorkflow.PROACTIVE, descriptor.getPsiElement(), project 34 | ); 35 | dialog.showAndGet(); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/intentions/FailedTypeChangeRecoveringIntention.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.intentions; 2 | 3 | import com.intellij.codeInsight.intention.PriorityAction; 4 | import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction; 5 | import com.intellij.codeInspection.util.IntentionFamilyName; 6 | import com.intellij.codeInspection.util.IntentionName; 7 | import com.intellij.openapi.editor.Editor; 8 | import com.intellij.openapi.project.Project; 9 | import com.intellij.psi.PsiElement; 10 | import com.intellij.psi.PsiExpression; 11 | import com.intellij.refactoring.typeMigration.TypeConversionDescriptor; 12 | import com.intellij.util.IncorrectOperationException; 13 | import org.jetbrains.annotations.NotNull; 14 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 15 | import org.jetbrains.research.ddtm.data.TypeChangeRulesStorage; 16 | import org.jetbrains.research.ddtm.data.models.TypeChangePatternDescriptor; 17 | import org.jetbrains.research.ddtm.data.models.TypeChangeRuleDescriptor; 18 | import org.jetbrains.research.ddtm.ide.fus.TypeChangeLogsCollector; 19 | import org.jetbrains.research.ddtm.ide.migration.collectors.TypeChangesInfoCollector; 20 | import org.jetbrains.research.ddtm.utils.PsiRelatedUtils; 21 | 22 | public class FailedTypeChangeRecoveringIntention extends PsiElementBaseIntentionAction implements PriorityAction { 23 | private final TypeChangeRuleDescriptor rule; 24 | 25 | public FailedTypeChangeRecoveringIntention(TypeChangeRuleDescriptor rule) { 26 | this.rule = rule; 27 | } 28 | 29 | @Override 30 | public @NotNull @IntentionFamilyName String getFamilyName() { 31 | return DataDrivenTypeMigrationBundle.message("intention.family.name"); 32 | } 33 | 34 | @Override 35 | public @IntentionName @NotNull String getText() { 36 | return rule.toString(); 37 | } 38 | 39 | @Override 40 | public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { 41 | return true; 42 | } 43 | 44 | @Override 45 | public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) 46 | throws IncorrectOperationException { 47 | final var conversionDescriptor = new TypeConversionDescriptor( 48 | rule.getExpressionBefore(), 49 | rule.getExpressionAfter() 50 | ); 51 | final var typeEvaluator = TypeChangesInfoCollector.getInstance().getTypeEvaluator(); 52 | conversionDescriptor.replace( 53 | PsiRelatedUtils.getHighestParentOfType(element, PsiExpression.class), 54 | typeEvaluator 55 | ); 56 | final var storage = project.getService(TypeChangeRulesStorage.class); 57 | final int patternId = storage.findPatternByRule(rule).map(TypeChangePatternDescriptor::getId).orElse(-1); 58 | TypeChangeLogsCollector.getInstance().recoveringIntentionApplied(project, patternId); 59 | } 60 | 61 | @Override 62 | public @NotNull Priority getPriority() { 63 | return PriorityAction.Priority.TOP; 64 | } 65 | } -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/intentions/ProactiveTypeChangeIntention.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.intentions; 2 | 3 | import com.intellij.codeInsight.intention.PriorityAction; 4 | import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction; 5 | import com.intellij.codeInspection.util.IntentionFamilyName; 6 | import com.intellij.codeInspection.util.IntentionName; 7 | import com.intellij.openapi.application.ApplicationManager; 8 | import com.intellij.openapi.editor.Editor; 9 | import com.intellij.openapi.project.Project; 10 | import com.intellij.psi.PsiElement; 11 | import com.intellij.psi.PsiType; 12 | import com.intellij.psi.PsiTypeElement; 13 | import com.intellij.util.IncorrectOperationException; 14 | import com.intellij.util.SlowOperations; 15 | import org.jetbrains.annotations.NotNull; 16 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 17 | import org.jetbrains.research.ddtm.data.TypeChangeRulesStorage; 18 | import org.jetbrains.research.ddtm.data.enums.InvocationWorkflow; 19 | import org.jetbrains.research.ddtm.ide.refactoring.TypeChangeDialog; 20 | import org.jetbrains.research.ddtm.utils.PsiRelatedUtils; 21 | 22 | import java.util.Objects; 23 | 24 | public class ProactiveTypeChangeIntention extends PsiElementBaseIntentionAction implements PriorityAction { 25 | @Override 26 | public @NotNull @IntentionFamilyName String getFamilyName() { 27 | return DataDrivenTypeMigrationBundle.message("intention.family.name"); 28 | } 29 | 30 | @Override 31 | public @IntentionName @NotNull String getText() { 32 | return this.getFamilyName(); 33 | } 34 | 35 | @Override 36 | public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { 37 | PsiTypeElement parentType = PsiRelatedUtils.getHighestParentOfType(element, PsiTypeElement.class); 38 | if (parentType != null) { 39 | String parentTypeQualifiedName = parentType.getType().getCanonicalText(); 40 | final var storage = project.getService(TypeChangeRulesStorage.class); 41 | return !storage.getPatternsBySourceType(parentTypeQualifiedName).isEmpty(); 42 | } 43 | return false; 44 | } 45 | 46 | @Override 47 | public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) 48 | throws IncorrectOperationException { 49 | PsiType rootType = Objects.requireNonNull(PsiRelatedUtils.getHighestParentOfType(element, PsiTypeElement.class)).getType(); 50 | final var storage = project.getService(TypeChangeRulesStorage.class); 51 | 52 | ApplicationManager.getApplication().invokeLater(() -> { 53 | final var dialog = new TypeChangeDialog( 54 | SlowOperations.allowSlowOperations(() -> storage.getPatternsBySourceType(rootType.getCanonicalText())), 55 | InvocationWorkflow.PROACTIVE, element, project 56 | ); 57 | dialog.showAndGet(); 58 | }); 59 | } 60 | 61 | @Override 62 | public @NotNull Priority getPriority() { 63 | return PriorityAction.Priority.TOP; 64 | } 65 | } -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/intentions/SuggestedTypeChangeIntention.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.intentions; 2 | 3 | import com.intellij.codeInsight.intention.PriorityAction; 4 | import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction; 5 | import com.intellij.codeInspection.util.IntentionFamilyName; 6 | import com.intellij.codeInspection.util.IntentionName; 7 | import com.intellij.openapi.editor.Editor; 8 | import com.intellij.openapi.project.Project; 9 | import com.intellij.psi.PsiElement; 10 | import com.intellij.util.IncorrectOperationException; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 13 | import org.jetbrains.research.ddtm.data.TypeChangeRulesStorage; 14 | import org.jetbrains.research.ddtm.ide.refactoring.TypeChangeGutterIconRenderer; 15 | import org.jetbrains.research.ddtm.ide.refactoring.TypeChangeMarker; 16 | 17 | public class SuggestedTypeChangeIntention extends PsiElementBaseIntentionAction implements PriorityAction { 18 | private final String sourceType; 19 | private final String targetType; 20 | 21 | public SuggestedTypeChangeIntention(TypeChangeMarker typeChangeMarker) { 22 | this.sourceType = typeChangeMarker.sourceType; 23 | this.targetType = typeChangeMarker.targetType; 24 | } 25 | 26 | @Override 27 | public @NotNull @IntentionFamilyName String getFamilyName() { 28 | return DataDrivenTypeMigrationBundle.message("intention.family.name"); 29 | } 30 | 31 | @Override 32 | public @IntentionName @NotNull String getText() { 33 | return DataDrivenTypeMigrationBundle.message("intention.suggested.text"); 34 | } 35 | 36 | @Override 37 | public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) { 38 | return true; 39 | } 40 | 41 | @Override 42 | public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) 43 | throws IncorrectOperationException { 44 | final var storage = project.getService(TypeChangeRulesStorage.class); 45 | final var pattern = storage.findPattern(sourceType, targetType); 46 | if (pattern.isEmpty()) return; 47 | 48 | (new TypeChangeGutterIconRenderer(element.getTextOffset())).showRefactoringOpportunity(project, editor); 49 | 50 | // ListPopup suggestionsPopup = JBPopupFactory.getInstance().createListPopup( 51 | // new TypeChangesListPopupStep( 52 | // DataDrivenTypeMigrationBundle.message("intention.list.caption"), 53 | // Collections.singletonList(pattern.get()), 54 | // element, 55 | // project, 56 | // InvocationWorkflow.REACTIVE 57 | // ) 58 | // ); 59 | // suggestionsPopup.showInBestPositionFor(editor); 60 | } 61 | 62 | @Override 63 | public @NotNull Priority getPriority() { 64 | return PriorityAction.Priority.TOP; 65 | } 66 | } -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/migration/HeuristicTypeConversionDescriptor.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.migration; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.psi.JavaPsiFacade; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.PsiExpression; 7 | import com.intellij.psi.codeStyle.JavaCodeStyleManager; 8 | import com.intellij.refactoring.typeMigration.TypeConversionDescriptor; 9 | import com.intellij.refactoring.typeMigration.TypeEvaluator; 10 | import com.intellij.structuralsearch.MatchOptions; 11 | import com.intellij.structuralsearch.MatchResult; 12 | import com.intellij.structuralsearch.plugin.replace.ReplaceOptions; 13 | import org.jetbrains.annotations.NonNls; 14 | import org.jetbrains.annotations.NotNull; 15 | import org.jetbrains.research.ddtm.Config; 16 | import org.jetbrains.research.ddtm.ide.migration.structuralsearch.MyReplacer; 17 | import org.jetbrains.research.ddtm.ide.migration.structuralsearch.SSRUtils; 18 | import org.jetbrains.research.ddtm.utils.PsiRelatedUtils; 19 | 20 | import java.util.List; 21 | 22 | public class HeuristicTypeConversionDescriptor extends TypeConversionDescriptor { 23 | private final String currentRootName; 24 | 25 | public HeuristicTypeConversionDescriptor(@NonNls String stringToReplace, 26 | @NonNls String replaceByString, 27 | @NonNls String currentRootName) { 28 | super(stringToReplace, replaceByString); 29 | this.currentRootName = currentRootName; 30 | } 31 | 32 | @Override 33 | public PsiExpression replace(PsiExpression expression, @NotNull TypeEvaluator evaluator) { 34 | Project project = expression.getProject(); 35 | PsiElement currentExpression = expression; 36 | PsiElement bestMatchedExpression = expression; 37 | int parentsPassed = 0; 38 | 39 | while (parentsPassed < Config.MAX_PARENTS_TO_LIFT_UP) { 40 | if (currentExpression instanceof PsiExpression) { 41 | List matches = SSRUtils.matchRule(getStringToReplace(), currentRootName, currentExpression, project); 42 | if (!matches.isEmpty() && 43 | matches.get(0).getChildren().stream() 44 | .noneMatch(matchResult -> !matchResult.getName().equals("1") 45 | && PsiRelatedUtils.hasRootInside(matchResult.getMatch(), currentRootName))) { 46 | bestMatchedExpression = currentExpression; 47 | } 48 | } 49 | currentExpression = currentExpression.getParent(); 50 | parentsPassed++; 51 | } 52 | 53 | final ReplaceOptions options = new ReplaceOptions(); 54 | final MatchOptions matchOptions = options.getMatchOptions(); 55 | SSRUtils.patchMatchOptionsWithConstraints(matchOptions, getStringToReplace(), currentRootName, bestMatchedExpression); 56 | 57 | final String replacement = MyReplacer.testReplace( 58 | bestMatchedExpression.getText(), getStringToReplace(), getReplaceByString(), options, project 59 | ); 60 | return (PsiExpression) JavaCodeStyleManager.getInstance(project).shortenClassReferences( 61 | bestMatchedExpression.replace( 62 | JavaPsiFacade.getElementFactory(project).createExpressionFromText(replacement, bestMatchedExpression) 63 | )); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/migration/HeuristicTypeConversionRule.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.migration; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.psi.*; 5 | import com.intellij.psi.util.PsiTreeUtil; 6 | import com.intellij.psi.util.PsiUtil; 7 | import com.intellij.refactoring.typeMigration.TypeConversionDescriptorBase; 8 | import com.intellij.refactoring.typeMigration.TypeMigrationLabeler; 9 | import com.intellij.refactoring.typeMigration.rules.TypeConversionRule; 10 | import com.intellij.refactoring.typeMigration.usageInfo.TypeMigrationUsageInfo; 11 | import com.intellij.structuralsearch.MatchResult; 12 | import org.jetbrains.annotations.ApiStatus; 13 | import org.jetbrains.annotations.Nullable; 14 | import org.jetbrains.research.ddtm.Config; 15 | import org.jetbrains.research.ddtm.data.TypeChangeRulesStorage; 16 | import org.jetbrains.research.ddtm.data.models.TypeChangeRuleDescriptor; 17 | import org.jetbrains.research.ddtm.ide.migration.collectors.RequiredImportsCollector; 18 | import org.jetbrains.research.ddtm.ide.migration.collectors.TypeChangesInfoCollector; 19 | import org.jetbrains.research.ddtm.ide.migration.structuralsearch.SSRUtils; 20 | import org.jetbrains.research.ddtm.utils.PsiRelatedUtils; 21 | 22 | import java.lang.reflect.Field; 23 | import java.util.List; 24 | 25 | public class HeuristicTypeConversionRule extends TypeConversionRule { 26 | @Override 27 | public @Nullable TypeConversionDescriptorBase findConversion( 28 | PsiType from, PsiType to, PsiMember member, PsiExpression context, TypeMigrationLabeler labeler 29 | ) { 30 | if (from.getCanonicalText().equals(to.getCanonicalText()) || context == null) return null; 31 | Project project = context.getProject(); 32 | final var collector = TypeChangesInfoCollector.getInstance(); 33 | 34 | final var storage = project.getService(TypeChangeRulesStorage.class); 35 | final var pattern = storage.findPattern( 36 | from.getCanonicalText(), 37 | to.getCanonicalText() 38 | ); 39 | 40 | final TypeMigrationUsageInfo currentRoot = extractCurrentRoot(labeler); 41 | if (pattern.isEmpty() || currentRoot == null) { 42 | collector.addFailedUsage(context); 43 | return null; 44 | } 45 | final String currentRootName = PsiUtil.getName(currentRoot.getElement()); 46 | 47 | PsiElement currentContext = context; 48 | int parentsPassed = 0; 49 | TypeChangeRuleDescriptor bestMatchedRule = null; 50 | final List rules = pattern.get().getRules(); 51 | 52 | while (parentsPassed < Config.MAX_PARENTS_TO_LIFT_UP) { 53 | // It means that we didn't lift up too much 54 | if (currentContext instanceof PsiExpression && !(currentContext instanceof PsiAssignmentExpression) && 55 | PsiTreeUtil.findChildrenOfType(currentContext, PsiReferenceExpression.class).stream() 56 | .filter(element -> element.getText().equals(currentRootName)).count() <= 1) { 57 | for (var rule : rules) { 58 | if (rule.getExpressionBefore().contains("$1$") && !PsiRelatedUtils.hasRootInside(currentContext, currentRootName)) { 59 | // To avoid cases when `currentRootName` appears in the `currentContext` as a part of some string literal, 60 | // such as root reference `file` in the expression `new File("file.txt")` 61 | continue; 62 | } 63 | List matches = SSRUtils.matchRule(rule.getExpressionBefore(), currentRootName, currentContext, project); 64 | if (!matches.isEmpty()) { 65 | // To prevent cases like `UUID.fromString(System.out.println(s))`, where `s` is current root, 66 | // and the rule $2$ -> UUId.fromString($2$) matches to all statement, which is wrong, 67 | // because it should not contain the root inside 68 | if (matches.get(0).getChildren().stream() 69 | .anyMatch(matchResult -> !matchResult.getName().equals("1") 70 | && PsiRelatedUtils.hasRootInside(matchResult.getMatch(), currentRootName))) { 71 | continue; 72 | } 73 | 74 | // Heuristic to prevent matching elements different from requested context 75 | final String match = matches.get(0).getMatch().getText(); 76 | if (member == null && !match.contains(context.getText())) { 77 | continue; 78 | } 79 | 80 | if (bestMatchedRule == null) { 81 | bestMatchedRule = rule; 82 | continue; 83 | } 84 | 85 | // Update bestMatchedRule iff it matches a larger number of tokens 86 | final var ruleTokens = PsiRelatedUtils.splitByTokens(rule.getExpressionBefore()); 87 | final var bestMatchedRuleTokens = PsiRelatedUtils.splitByTokens(bestMatchedRule.getExpressionBefore()); 88 | if (bestMatchedRuleTokens.length < ruleTokens.length 89 | || bestMatchedRuleTokens.length == ruleTokens.length && rule.getExpressionBefore().contains("$1$")) { 90 | // The second condition is used to always prefer rules with a current root in the "before" part 91 | bestMatchedRule = rule; 92 | } 93 | } 94 | } 95 | } 96 | currentContext = currentContext.getParent(); 97 | parentsPassed++; 98 | } 99 | 100 | if (bestMatchedRule != null) { 101 | if (bestMatchedRule.getReturnType() != null) { 102 | // Check if the rule is "suspicious", e.g. it changes the return type of the expression 103 | if (!bestMatchedRule.getReturnType().getSourceType().equals(bestMatchedRule.getReturnType().getTargetType())) { 104 | collector.addFailedUsage(context); 105 | collector.addRuleForFailedUsage(context, bestMatchedRule); 106 | return null; 107 | } 108 | } 109 | // Collect required imports for this rule 110 | if (bestMatchedRule.getRequiredImports() != null) { 111 | RequiredImportsCollector.getInstance().addRequiredImport(bestMatchedRule.getRequiredImports()); 112 | } 113 | // Will be successfully updated with a rule 114 | collector.addUpdatedUsage(context); 115 | collector.addUsedRule(bestMatchedRule); 116 | return new HeuristicTypeConversionDescriptor( 117 | bestMatchedRule.getExpressionBefore(), 118 | bestMatchedRule.getExpressionAfter(), 119 | currentRootName 120 | ); 121 | } 122 | collector.addFailedUsage(context); 123 | return null; 124 | } 125 | 126 | @ApiStatus.Internal 127 | private @Nullable TypeMigrationUsageInfo extractCurrentRoot(TypeMigrationLabeler labeler) { 128 | try { 129 | Field field = labeler.getClass().getDeclaredField("myCurrentRoot"); 130 | field.setAccessible(true); 131 | return (TypeMigrationUsageInfo) field.get(labeler); 132 | } catch (IllegalAccessException | NoSuchFieldException e) { 133 | e.printStackTrace(); 134 | } 135 | return null; 136 | } 137 | } -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/migration/collectors/RequiredImportsCollector.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.migration.collectors; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | public class RequiredImportsCollector extends SwitchableCollector { 7 | private static RequiredImportsCollector collector = null; 8 | private final Set requiredImports; 9 | 10 | private RequiredImportsCollector() { 11 | this.requiredImports = new HashSet<>(); 12 | } 13 | 14 | public static RequiredImportsCollector getInstance() { 15 | if (collector == null) { 16 | collector = new RequiredImportsCollector(); 17 | } 18 | return collector; 19 | } 20 | 21 | public void addRequiredImport(String requiredImport) { 22 | if (shouldCollect) { 23 | requiredImports.add(requiredImport); 24 | } 25 | } 26 | 27 | public Set getRequiredImports() { 28 | return requiredImports; 29 | } 30 | 31 | public void clear() { 32 | requiredImports.clear(); 33 | shouldCollect = true; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/migration/collectors/SwitchableCollector.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.migration.collectors; 2 | 3 | public abstract class SwitchableCollector { 4 | protected boolean shouldCollect; 5 | 6 | public void on() { 7 | shouldCollect = true; 8 | } 9 | 10 | public void off() { 11 | shouldCollect = false; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/migration/collectors/TypeChangesInfoCollector.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.migration.collectors; 2 | 3 | import com.intellij.psi.PsiElement; 4 | import com.intellij.psi.PsiManager; 5 | import com.intellij.psi.SmartPointerManager; 6 | import com.intellij.psi.SmartPsiElementPointer; 7 | import com.intellij.refactoring.typeMigration.TypeEvaluator; 8 | import com.intellij.refactoring.typeMigration.TypeMigrationProcessor; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.research.ddtm.data.models.TypeChangeRuleDescriptor; 11 | 12 | import java.util.*; 13 | import java.util.stream.Collectors; 14 | 15 | public class TypeChangesInfoCollector extends SwitchableCollector { 16 | private static final int MAX_PARENTS_TO_LIFT_UP = 4; 17 | 18 | private static TypeChangesInfoCollector collector = null; 19 | private TypeEvaluator typeEvaluator; 20 | 21 | private final List> updatedUsages = new ArrayList<>(); 22 | private final List> failedUsages = new ArrayList<>(); 23 | private final Map, TypeChangeRuleDescriptor> failedUsageToCorrespondingRule = new HashMap<>(); 24 | private final Set usedRules = new HashSet<>(); 25 | 26 | public static TypeChangesInfoCollector getInstance() { 27 | if (collector == null) { 28 | collector = new TypeChangesInfoCollector(); 29 | } 30 | return collector; 31 | } 32 | 33 | public List> getUpdatedUsages() { 34 | return updatedUsages; 35 | } 36 | 37 | public List> getFailedUsages() { 38 | return failedUsages; 39 | } 40 | 41 | public Set getUsedRules() { 42 | return usedRules; 43 | } 44 | 45 | public List> getSuspiciousUsages() { 46 | return failedUsages.stream() 47 | .filter(failedUsageToCorrespondingRule::containsKey) 48 | .collect(Collectors.toList()); 49 | } 50 | 51 | public void addUsedRule(TypeChangeRuleDescriptor rule) { 52 | if (shouldCollect) { 53 | usedRules.add(rule); 54 | } 55 | } 56 | 57 | public void addFailedUsage(PsiElement element) { 58 | if (shouldCollect) { 59 | failedUsages.add(SmartPointerManager.createPointer(element)); 60 | } 61 | } 62 | 63 | public void addUpdatedUsage(PsiElement element) { 64 | if (shouldCollect) { 65 | updatedUsages.add(SmartPointerManager.createPointer(element)); 66 | } 67 | } 68 | 69 | public void addRuleForFailedUsage(PsiElement element, TypeChangeRuleDescriptor rule) { 70 | if (shouldCollect) { 71 | final var pointer = SmartPointerManager.createPointer(element); 72 | failedUsageToCorrespondingRule.put(pointer, rule); 73 | } 74 | } 75 | 76 | public Optional getRuleForFailedUsage(@NotNull PsiElement element) { 77 | int parentsPassed = 0; 78 | PsiElement currentContext = element; 79 | final var psiManager = PsiManager.getInstance(element.getProject()); 80 | 81 | while (currentContext != null && parentsPassed < MAX_PARENTS_TO_LIFT_UP) { 82 | for (var entry : failedUsageToCorrespondingRule.entrySet()) { 83 | final boolean arePsiElementsEqual = psiManager.areElementsEquivalent( 84 | entry.getKey().getElement(), 85 | currentContext 86 | ); 87 | if (arePsiElementsEqual) { 88 | return Optional.of(entry.getValue()); 89 | } 90 | } 91 | currentContext = currentContext.getParent(); 92 | parentsPassed++; 93 | } 94 | return Optional.empty(); 95 | } 96 | 97 | /** 98 | * Clears the list of usages failed to migrate, collected during previous type migration pass. 99 | *

100 | * Call this method before calling built-in {@link TypeMigrationProcessor#findUsages()} 101 | *

102 | */ 103 | public void clear() { 104 | failedUsages.clear(); 105 | updatedUsages.clear(); 106 | failedUsageToCorrespondingRule.clear(); 107 | usedRules.clear(); 108 | shouldCollect = true; 109 | } 110 | 111 | public boolean hasFailedTypeChanges() { 112 | return !failedUsages.isEmpty(); 113 | } 114 | 115 | public TypeEvaluator getTypeEvaluator() { 116 | return typeEvaluator; 117 | } 118 | 119 | public void setTypeEvaluator(TypeEvaluator typeEvaluator) { 120 | this.typeEvaluator = typeEvaluator; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/migration/structuralsearch/MyReplacer.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.migration.structuralsearch; 2 | 3 | import com.intellij.lang.Language; 4 | import com.intellij.openapi.fileTypes.LanguageFileType; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.psi.PsiElement; 7 | import com.intellij.psi.PsiFile; 8 | import com.intellij.psi.PsiWhiteSpace; 9 | import com.intellij.psi.search.LocalSearchScope; 10 | import com.intellij.structuralsearch.MatchOptions; 11 | import com.intellij.structuralsearch.MatchResult; 12 | import com.intellij.structuralsearch.Matcher; 13 | import com.intellij.structuralsearch.PatternContextInfo; 14 | import com.intellij.structuralsearch.impl.matcher.MatcherImplUtil; 15 | import com.intellij.structuralsearch.impl.matcher.PatternTreeContext; 16 | import com.intellij.structuralsearch.plugin.replace.ReplaceOptions; 17 | import com.intellij.structuralsearch.plugin.replace.ReplacementInfo; 18 | import com.intellij.structuralsearch.plugin.replace.impl.Replacer; 19 | import com.intellij.structuralsearch.plugin.util.CollectingMatchResultSink; 20 | import com.intellij.util.IncorrectOperationException; 21 | import com.intellij.util.SmartList; 22 | import org.jetbrains.annotations.NotNull; 23 | 24 | import java.util.List; 25 | 26 | public class MyReplacer extends Replacer { 27 | public MyReplacer(@NotNull Project project, @NotNull ReplaceOptions options) { 28 | super(project, options); 29 | } 30 | 31 | public static String testReplace(String in, String what, String by, ReplaceOptions replaceOptions, Project project) { 32 | final LanguageFileType type = replaceOptions.getMatchOptions().getFileType(); 33 | return testReplace(in, what, by, replaceOptions, project, false, false, type, type.getLanguage()); 34 | } 35 | 36 | /** 37 | * Copied from {@link Replacer#testReplace(java.lang.String, java.lang.String, java.lang.String, com.intellij.structuralsearch.plugin.replace.ReplaceOptions, com.intellij.openapi.project.Project, boolean, boolean, com.intellij.openapi.fileTypes.LanguageFileType, com.intellij.lang.Language)}. 38 | * But it doesn't clears `searchCriteria` in the MatchOptions. 39 | */ 40 | public static String testReplace(String in, String what, String by, ReplaceOptions replaceOptions, Project project, boolean sourceIsFile, 41 | boolean createPhysicalFile, @NotNull LanguageFileType sourceFileType, @NotNull Language sourceDialect) { 42 | replaceOptions.setReplacement(by); 43 | 44 | final MatchOptions matchOptions = replaceOptions.getMatchOptions(); 45 | 46 | Matcher.validate(project, matchOptions); 47 | checkReplacementPattern(project, replaceOptions); 48 | 49 | final Replacer replacer = new Replacer(project, replaceOptions); 50 | final Matcher matcher = new Matcher(project, matchOptions); 51 | try { 52 | final PsiElement firstElement; 53 | final PsiElement lastElement; 54 | final PsiElement parent; 55 | if (matchOptions.getScope() == null) { 56 | final PsiElement[] elements = MatcherImplUtil.createTreeFromText( 57 | in, 58 | new PatternContextInfo(sourceIsFile ? PatternTreeContext.File : PatternTreeContext.Block), 59 | sourceFileType, 60 | sourceDialect, 61 | project, 62 | createPhysicalFile 63 | ); 64 | 65 | firstElement = elements[0]; 66 | lastElement = elements[elements.length - 1]; 67 | parent = firstElement.getParent(); 68 | 69 | matchOptions.setScope(new LocalSearchScope(elements)); 70 | } else { 71 | parent = ((LocalSearchScope) matchOptions.getScope()).getScope()[0]; 72 | firstElement = parent.getFirstChild(); 73 | lastElement = parent.getLastChild(); 74 | } 75 | 76 | final CollectingMatchResultSink sink = new CollectingMatchResultSink(); 77 | matcher.testFindMatches(sink); 78 | 79 | final List replacements = new SmartList<>(); 80 | for (final MatchResult result : sink.getMatches()) { 81 | replacements.add(replacer.buildReplacement(result)); 82 | } 83 | 84 | int startOffset = firstElement.getTextRange().getStartOffset(); 85 | int endOffset = sourceIsFile ? 0 : (parent.getTextLength() - lastElement.getTextRange().getEndOffset()); 86 | 87 | // get nodes from text may contain 88 | final PsiElement prevSibling = firstElement.getPrevSibling(); 89 | if (prevSibling instanceof PsiWhiteSpace) { 90 | startOffset -= prevSibling.getTextLength(); 91 | } 92 | 93 | final PsiElement nextSibling = lastElement.getNextSibling(); 94 | if (nextSibling instanceof PsiWhiteSpace) { 95 | endOffset -= nextSibling.getTextLength(); 96 | } 97 | replacer.replaceAll(replacements); 98 | if (firstElement == lastElement && firstElement instanceof PsiFile) { 99 | return firstElement.getText(); 100 | } 101 | final String result = parent.getText(); 102 | return result.substring(startOffset, result.length() - endOffset); 103 | } catch (RuntimeException e) { 104 | throw e; 105 | } catch (Exception e) { 106 | throw new IncorrectOperationException(e); 107 | } finally { 108 | matchOptions.setScope(null); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/migration/structuralsearch/SSRUtils.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.migration.structuralsearch; 2 | 3 | import com.intellij.ide.highlighter.JavaFileType; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.psi.JavaPsiFacade; 6 | import com.intellij.psi.PsiElement; 7 | import com.intellij.psi.PsiType; 8 | import com.intellij.structuralsearch.MatchOptions; 9 | import com.intellij.structuralsearch.MatchResult; 10 | import com.intellij.structuralsearch.MatchVariableConstraint; 11 | import com.intellij.structuralsearch.Matcher; 12 | import com.intellij.structuralsearch.impl.matcher.CompiledPattern; 13 | import com.intellij.structuralsearch.impl.matcher.compiler.PatternCompiler; 14 | import com.intellij.structuralsearch.plugin.replace.ReplaceOptions; 15 | import com.intellij.structuralsearch.plugin.replace.impl.Replacer; 16 | import org.jetbrains.annotations.NotNull; 17 | 18 | import java.util.List; 19 | import java.util.regex.Pattern; 20 | 21 | public class SSRUtils { 22 | public static List matchRule(String pattern, String currentRootName, PsiElement sourceExpression, Project project) { 23 | final MatchOptions options = new MatchOptions(); 24 | patchMatchOptionsWithConstraints(options, pattern, currentRootName, sourceExpression); 25 | final CompiledPattern compiledPattern = PatternCompiler.compilePattern( 26 | project, options, false, false 27 | ); 28 | final Matcher matcher = new Matcher(project, options, compiledPattern); 29 | return matcher.testFindMatches(sourceExpression.getText(), false, JavaFileType.INSTANCE, false); 30 | } 31 | 32 | public static void patchMatchOptionsWithConstraints(MatchOptions options, String pattern, String currentRootName, PsiElement sourceExpression) { 33 | options.fillSearchCriteria(pattern); 34 | options.setFileType(JavaFileType.INSTANCE); 35 | 36 | String src = sourceExpression.getText(); 37 | String regexForRoot = currentRootName; 38 | int startIndex = src.indexOf(currentRootName); 39 | if (startIndex != -1) { 40 | int i = startIndex + currentRootName.length(); 41 | int balance = 0; 42 | while (i < src.length()) { 43 | if (src.charAt(i) == '(') balance++; 44 | if (src.charAt(i) == ')') balance--; 45 | if (balance == 0) break; 46 | i++; 47 | } 48 | if (balance == 0) { 49 | // If the current root is a function or method, it will help to match it with a "full inclusion", 50 | // i.e., if the current root is `someFunc`, then it will match it correctly within call expression 51 | // `someFunc(someStr.toLowerCase()).toUpperCase()` because of the balanced bracket sequence. 52 | if (i < src.length() && src.charAt(i) == ')') i++; 53 | regexForRoot = Pattern.quote(src.substring(startIndex, i)); 54 | } 55 | } 56 | 57 | MatchVariableConstraint rootConstraint = new MatchVariableConstraint("1"); 58 | rootConstraint.setRegExp(regexForRoot); 59 | options.addVariableConstraint(rootConstraint); 60 | 61 | // There should be a regex retrieving all the variables between $$, 62 | // but I guess it's enough to only add constraints for 2, 3, 4, 5 with current templates 63 | for (int i = 2; i < 5; ++i) { 64 | MatchVariableConstraint nonRootConstraint = new MatchVariableConstraint(Integer.toString(i)); 65 | nonRootConstraint.setRegExp(regexForRoot); 66 | nonRootConstraint.setInvertRegExp(true); 67 | options.addVariableConstraint(nonRootConstraint); 68 | } 69 | } 70 | 71 | public static List matchType(String source, String typePattern, Project project) { 72 | final MatchOptions options = new MatchOptions(); 73 | options.setSearchPattern(typePattern); 74 | options.setFileType(JavaFileType.INSTANCE); 75 | final Matcher matcher = new Matcher(project, options); 76 | return matcher.testFindMatches(source, false, JavaFileType.INSTANCE, false); 77 | } 78 | 79 | @NotNull 80 | public static String substituteTypeByPattern(@NotNull PsiType type, 81 | String stringToSubstitute, 82 | String substituteByString, 83 | Project project) { 84 | final ReplaceOptions options = new ReplaceOptions(); 85 | final MatchOptions matchOptions = options.getMatchOptions(); 86 | matchOptions.setFileType(JavaFileType.INSTANCE); 87 | String result = Replacer.testReplace( 88 | type.getCanonicalText() + " x;", 89 | stringToSubstitute + " x;", 90 | substituteByString + " x;", 91 | options, project 92 | ); 93 | return result.substring(0, result.length() - 3); 94 | } 95 | 96 | public static Boolean hasMatch(String source, String typePattern, Project project) { 97 | // Full match 98 | if (typePattern.equals(source)) return true; 99 | 100 | // Preventing incorrect matches for generic types, such as from List to String 101 | if (source.contains(typePattern)) return false; 102 | 103 | // Matching complicated cases with substitutions, such as List to List<$1$> 104 | if (!SSRUtils.matchType(source, typePattern, project).isEmpty()) return true; 105 | 106 | // Match supertypes, such as ArrayList<> to List<> 107 | PsiType sourceType = JavaPsiFacade.getElementFactory(project).createTypeFromText(source, null); 108 | for (PsiType superType : sourceType.getSuperTypes()) { 109 | if (!SSRUtils.matchType(superType.getCanonicalText(), typePattern, project).isEmpty()) return true; 110 | } 111 | return false; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/refactoring/ReactiveTypeChangeAvailabilityUpdater.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.refactoring; 2 | 3 | import com.intellij.openapi.components.Service; 4 | import com.intellij.openapi.editor.Document; 5 | import com.intellij.openapi.editor.Editor; 6 | import com.intellij.openapi.editor.EditorFactory; 7 | import com.intellij.openapi.editor.markup.HighlighterLayer; 8 | import com.intellij.openapi.editor.markup.HighlighterTargetArea; 9 | import com.intellij.openapi.editor.markup.RangeHighlighter; 10 | import com.intellij.openapi.project.Project; 11 | import org.jetbrains.research.ddtm.ide.refactoring.services.TypeChangeRefactoringProvider; 12 | 13 | import java.util.HashMap; 14 | import java.util.Map; 15 | 16 | @Service 17 | public final class ReactiveTypeChangeAvailabilityUpdater { 18 | private final Project project; 19 | private final Map editorsAndHighlighters = new HashMap<>(); 20 | 21 | public ReactiveTypeChangeAvailabilityUpdater(Project project) { 22 | this.project = project; 23 | } 24 | 25 | public void updateAllHighlighters(Document document, int caretOffset) { 26 | EditorFactory.getInstance().editors(document, project).forEach(editor -> updateHighlighter(editor, caretOffset)); 27 | } 28 | 29 | public void updateHighlighter(Editor editor, int caretOffset) { 30 | final var state = TypeChangeRefactoringProvider.getInstance(project).getState(); 31 | final var relevantTypeChangeForCurrentOffset = state.getCompletedTypeChangeForOffset(caretOffset); 32 | 33 | final var prevHighlighter = editorsAndHighlighters.get(editor); 34 | if (prevHighlighter != null) { 35 | editor.getMarkupModel().removeHighlighter(prevHighlighter); 36 | editorsAndHighlighters.remove(editor); 37 | } 38 | 39 | if (state.refactoringEnabled && relevantTypeChangeForCurrentOffset.isPresent()) { 40 | final var highlighterRangeMarker = relevantTypeChangeForCurrentOffset.get().newRangeMarker; 41 | final var highlighter = editor.getMarkupModel().addRangeHighlighter( 42 | null, 43 | highlighterRangeMarker.getStartOffset() + 1, 44 | highlighterRangeMarker.getEndOffset() + 1, 45 | HighlighterLayer.LAST, 46 | HighlighterTargetArea.EXACT_RANGE 47 | ); 48 | highlighter.setGutterIconRenderer(new TypeChangeGutterIconRenderer(caretOffset)); 49 | editorsAndHighlighters.put(editor, highlighter); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/refactoring/TypeChangeDialog.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.refactoring; 2 | 3 | import com.intellij.openapi.progress.ProgressManager; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.openapi.ui.DialogWrapper; 6 | import com.intellij.psi.PsiElement; 7 | import com.intellij.psi.PsiType; 8 | import org.jetbrains.annotations.Nullable; 9 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 10 | import org.jetbrains.research.ddtm.data.enums.InvocationWorkflow; 11 | import org.jetbrains.research.ddtm.data.models.TypeChangePatternDescriptor; 12 | import org.jetbrains.research.ddtm.ide.migration.TypeChangeProcessor; 13 | import org.jetbrains.research.ddtm.ide.settings.TypeChangeSettingsState; 14 | import org.jetbrains.research.ddtm.ide.ui.TypeChangeClassicModePanel; 15 | import org.jetbrains.research.ddtm.utils.PsiRelatedUtils; 16 | 17 | import javax.swing.*; 18 | import java.util.Comparator; 19 | import java.util.List; 20 | import java.util.Objects; 21 | import java.util.stream.Collectors; 22 | 23 | public class TypeChangeDialog extends DialogWrapper { 24 | private final InvocationWorkflow invocationWorkflow; 25 | private final Project project; 26 | private final PsiElement context; 27 | private final TypeChangeClassicModePanel panel; 28 | 29 | public TypeChangeDialog(List rulesDescriptors, 30 | InvocationWorkflow invocationWorkflow, PsiElement context, Project project) { 31 | super(true); 32 | this.project = project; 33 | this.invocationWorkflow = invocationWorkflow; 34 | this.context = context; 35 | PsiType sourceType = Objects.requireNonNull(PsiRelatedUtils.getClosestPsiTypeElement(context)).getType(); 36 | 37 | this.panel = new TypeChangeClassicModePanel( 38 | rulesDescriptors.stream() 39 | .sorted(Comparator.comparing(TypeChangePatternDescriptor::getRank).reversed()) 40 | .collect(Collectors.toList()), 41 | sourceType, 42 | project 43 | ); 44 | setTitle(DataDrivenTypeMigrationBundle.message("settings.display.name")); 45 | setOKButtonText(DataDrivenTypeMigrationBundle.message("suggested.gutter.popup.button")); 46 | init(); 47 | } 48 | 49 | @Override 50 | protected void doOKAction() { 51 | super.doOKAction(); 52 | final var processor = new TypeChangeProcessor(project, invocationWorkflow); 53 | TypeChangeSettingsState.getInstance().searchScope = panel.getSearchScopeOption(); 54 | 55 | ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { 56 | ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); 57 | processor.run(context, panel.getSelectedPatternDescriptor()); 58 | }, DataDrivenTypeMigrationBundle.message("intention.family.name"), true, project); 59 | } 60 | 61 | @Override 62 | protected @Nullable JComponent createCenterPanel() { 63 | return panel; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/refactoring/TypeChangeGutterIconRenderer.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.refactoring; 2 | 3 | import com.intellij.icons.AllIcons; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import com.intellij.openapi.actionSystem.CommonDataKeys; 7 | import com.intellij.openapi.actionSystem.CustomShortcutSet; 8 | import com.intellij.openapi.application.impl.LaterInvocator; 9 | import com.intellij.openapi.editor.Editor; 10 | import com.intellij.openapi.editor.ex.EditorEx; 11 | import com.intellij.openapi.editor.markup.GutterIconRenderer; 12 | import com.intellij.openapi.progress.ProgressManager; 13 | import com.intellij.openapi.project.DumbAwareAction; 14 | import com.intellij.openapi.project.Project; 15 | import com.intellij.openapi.ui.popup.Balloon; 16 | import com.intellij.openapi.ui.popup.JBPopupFactory; 17 | import com.intellij.openapi.util.Disposer; 18 | import com.intellij.psi.PsiDocumentManager; 19 | import com.intellij.psi.PsiElement; 20 | import com.intellij.psi.PsiFile; 21 | import com.intellij.ui.awt.RelativePoint; 22 | import com.intellij.util.ui.JBUI; 23 | import org.jetbrains.annotations.NotNull; 24 | import org.jetbrains.annotations.Nullable; 25 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 26 | import org.jetbrains.research.ddtm.data.TypeChangeRulesStorage; 27 | import org.jetbrains.research.ddtm.data.enums.InvocationWorkflow; 28 | import org.jetbrains.research.ddtm.data.models.TypeChangePatternDescriptor; 29 | import org.jetbrains.research.ddtm.ide.fus.TypeChangeLogsCollector; 30 | import org.jetbrains.research.ddtm.ide.migration.TypeChangeProcessor; 31 | import org.jetbrains.research.ddtm.ide.refactoring.services.TypeChangeRefactoringProvider; 32 | import org.jetbrains.research.ddtm.ide.settings.TypeChangeSettingsState; 33 | import org.jetbrains.research.ddtm.ide.ui.TypeChangeGutterPopupPanel; 34 | 35 | import javax.swing.*; 36 | import java.awt.*; 37 | 38 | public class TypeChangeGutterIconRenderer extends GutterIconRenderer { 39 | private final int offset; 40 | 41 | public TypeChangeGutterIconRenderer(int offset) { 42 | this.offset = offset; 43 | } 44 | 45 | @Override 46 | public @NotNull Icon getIcon() { 47 | return AllIcons.Gutter.SuggestedRefactoringBulb; 48 | } 49 | 50 | @Override 51 | public @Nullable String getTooltipText() { 52 | return DataDrivenTypeMigrationBundle.message("suggested.gutter.icon.tooltip.text"); 53 | } 54 | 55 | @Override 56 | public boolean isNavigateAction() { 57 | return true; 58 | } 59 | 60 | @Override 61 | public @NotNull Alignment getAlignment() { 62 | return Alignment.RIGHT; 63 | } 64 | 65 | @Override 66 | public @Nullable AnAction getClickAction() { 67 | return new AnAction() { 68 | @Override 69 | public void actionPerformed(@NotNull AnActionEvent e) { 70 | final var project = e.getDataContext().getData(CommonDataKeys.PROJECT); 71 | final var editor = e.getDataContext().getData(CommonDataKeys.EDITOR); 72 | showRefactoringOpportunity(project, editor); 73 | } 74 | }; 75 | } 76 | 77 | public void showRefactoringOpportunity(Project project, Editor editor) { 78 | final var state = TypeChangeRefactoringProvider.getInstance(project).getState(); 79 | final var optionalTypeChangeMarker = state.getCompletedTypeChangeForOffset(offset); 80 | if (optionalTypeChangeMarker.isEmpty()) return; 81 | final var typeChangeMarker = optionalTypeChangeMarker.get(); 82 | 83 | final PsiFile psiFile = PsiDocumentManager.getInstance(project).getCachedPsiFile(editor.getDocument()); 84 | if (psiFile == null) return; 85 | final PsiElement newElement = psiFile.findElementAt(offset); 86 | 87 | final var storage = project.getService(TypeChangeRulesStorage.class); 88 | final var pattern = storage.findPattern(typeChangeMarker.sourceType, typeChangeMarker.targetType); 89 | if (pattern.isEmpty()) return; 90 | 91 | final var data = SuggestedRefactoringData.getInstance(); 92 | data.project = project; 93 | data.context = newElement; 94 | data.pattern = pattern.get(); 95 | 96 | final var popup = new TypeChangeGutterPopupPanel(typeChangeMarker.sourceType, typeChangeMarker.targetType); 97 | final BalloonCallback callback = createAndShowBalloon( 98 | popup, 99 | editor, 100 | this::doRefactoring 101 | ); 102 | popup.onRefactor = callback.onApply; 103 | } 104 | 105 | private void doRefactoring() { 106 | final var data = SuggestedRefactoringData.getInstance(); 107 | final var processor = new TypeChangeProcessor(data.project, InvocationWorkflow.REACTIVE); 108 | TypeChangeLogsCollector.getInstance().gutterIconClicked(); 109 | ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { 110 | ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); 111 | processor.run(data.context, data.pattern); 112 | }, DataDrivenTypeMigrationBundle.message("intention.family.name"), true, data.project); 113 | } 114 | 115 | private BalloonCallback createAndShowBalloon(TypeChangeGutterPopupPanel panel, Editor editor, Runnable doRefactoring) { 116 | final var builder = JBPopupFactory.getInstance() 117 | .createDialogBalloonBuilder(panel, null) 118 | .setRequestFocus(true) 119 | .setHideOnClickOutside(true) 120 | .setCloseButtonEnabled(false) 121 | .setAnimationCycle(0) 122 | .setBlockClicksThroughBalloon(true) 123 | .setContentInsets(JBUI.emptyInsets()); 124 | final var borderColor = UIManager.getColor("InplaceRefactoringPopup.borderColor"); 125 | if (borderColor != null) { 126 | builder.setBorderColor(borderColor); 127 | } 128 | 129 | final var balloon = builder.createBalloon(); 130 | 131 | Runnable hideBalloonAndRefactor = () -> { 132 | balloon.hide(true); 133 | TypeChangeSettingsState.getInstance().searchScope = panel.getSearchScopeOption(); 134 | doRefactoring.run(); 135 | }; 136 | 137 | final var gutterComponent = ((EditorEx) editor).getGutterComponentEx(); 138 | final var anchor = gutterComponent.getCenterPoint(this); 139 | if (anchor != null) { 140 | balloon.show(new RelativePoint(gutterComponent, anchor), Balloon.Position.below); 141 | } else { 142 | final var caretXY = editor.offsetToXY(editor.getCaretModel().getOffset()); 143 | final var top = new RelativePoint(editor.getContentComponent(), caretXY); 144 | balloon.show( 145 | new RelativePoint(editor.getContentComponent(), new Point(caretXY.x, top.getOriginalPoint().y)), 146 | Balloon.Position.above 147 | ); 148 | } 149 | 150 | new DumbAwareAction() { 151 | @Override 152 | public void actionPerformed(@NotNull AnActionEvent e) { 153 | hideBalloonAndRefactor.run(); 154 | } 155 | }.registerCustomShortcutSet(CustomShortcutSet.fromString("ENTER"), panel, balloon); 156 | 157 | new DumbAwareAction() { 158 | @Override 159 | public void actionPerformed(@NotNull AnActionEvent e) { 160 | balloon.hide(false); 161 | } 162 | }.registerCustomShortcutSet(CustomShortcutSet.fromString("ESCAPE"), panel, balloon); 163 | 164 | LaterInvocator.enterModal(balloon); 165 | Disposer.register(balloon, () -> LaterInvocator.leaveModal(balloon)); 166 | 167 | return new BalloonCallback(hideBalloonAndRefactor); 168 | } 169 | 170 | 171 | @Override 172 | public boolean equals(Object obj) { 173 | return this == obj; 174 | } 175 | 176 | @Override 177 | public int hashCode() { 178 | return 0; 179 | } 180 | } 181 | 182 | class SuggestedRefactoringData { 183 | private static volatile SuggestedRefactoringData INSTANCE; 184 | 185 | public Project project; 186 | public PsiElement context; 187 | public TypeChangePatternDescriptor pattern; 188 | 189 | private SuggestedRefactoringData() { 190 | } 191 | 192 | public static SuggestedRefactoringData getInstance() { 193 | return Holder.INSTANCE; 194 | } 195 | 196 | private static class Holder { 197 | private static final SuggestedRefactoringData INSTANCE = new SuggestedRefactoringData(); 198 | } 199 | } 200 | 201 | class BalloonCallback { 202 | public Runnable onApply; 203 | 204 | public BalloonCallback(Runnable onApply) { 205 | this.onApply = onApply; 206 | } 207 | } -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/refactoring/TypeChangeMarker.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.refactoring; 2 | 3 | import com.intellij.openapi.editor.RangeMarker; 4 | 5 | public class TypeChangeMarker { 6 | public RangeMarker oldRangeMarker; 7 | public RangeMarker newRangeMarker; 8 | public String sourceType; 9 | public String targetType; 10 | 11 | public TypeChangeMarker(RangeMarker oldRangeMarker, RangeMarker newRangeMarker, String sourceType, String targetType) { 12 | this.oldRangeMarker = oldRangeMarker; 13 | this.newRangeMarker = newRangeMarker; 14 | this.sourceType = sourceType; 15 | this.targetType = targetType; 16 | } 17 | 18 | @Override 19 | public int hashCode() { 20 | int result = oldRangeMarker.hashCode(); 21 | result = 31 * result + newRangeMarker.hashCode(); 22 | result = 31 * result + sourceType.hashCode(); 23 | result = 31 * result + targetType.hashCode(); 24 | return result; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object obj) { 29 | if (!(obj instanceof TypeChangeMarker)) return false; 30 | final TypeChangeMarker other = (TypeChangeMarker) obj; 31 | if (other == this) return true; 32 | return other.sourceType.equals(this.sourceType) 33 | && other.targetType.equals(this.targetType) 34 | && other.oldRangeMarker.equals(this.oldRangeMarker) 35 | && other.newRangeMarker.equals(this.newRangeMarker); 36 | } 37 | } -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/refactoring/TypeChangeSuggestedRefactoringState.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.refactoring; 2 | 3 | import com.intellij.openapi.editor.RangeMarker; 4 | import com.intellij.openapi.util.TextRange; 5 | import org.jetbrains.research.ddtm.utils.EditorUtils; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | 13 | public class TypeChangeSuggestedRefactoringState { 14 | // TODO: encapsulate 15 | public final Map uncompletedTypeChanges; 16 | public final List completedTypeChanges; 17 | 18 | public volatile boolean refactoringEnabled; 19 | public volatile boolean isInternalTypeChangeInProgress = false; 20 | 21 | public TypeChangeSuggestedRefactoringState() { 22 | this.refactoringEnabled = false; 23 | this.uncompletedTypeChanges = new ConcurrentHashMap<>(); 24 | this.completedTypeChanges = new ArrayList<>(); 25 | } 26 | 27 | public Optional getCompletedTypeChangeForOffset(int offset) { 28 | return completedTypeChanges.stream() 29 | .filter(it -> it.newRangeMarker.getStartOffset() <= offset && offset < it.newRangeMarker.getEndOffset()) 30 | .reduce((x, y) -> y); // ~ findLast() 31 | } 32 | 33 | public boolean hasUncompletedTypeChangeForOffset(int offset) { 34 | return uncompletedTypeChanges.keySet().stream() 35 | .anyMatch(it -> it.getStartOffset() <= offset && offset < it.getEndOffset()); 36 | } 37 | 38 | public void addCompletedTypeChange(RangeMarker relevantOldRange, RangeMarker newRange, 39 | String relevantSourceType, String targetType) { 40 | final var typeChange = new TypeChangeMarker(relevantOldRange, newRange, relevantSourceType, targetType); 41 | completedTypeChanges.add(typeChange); 42 | } 43 | 44 | public void removeAllTypeChangesByRange(TextRange range) { 45 | uncompletedTypeChanges.keySet().removeIf(oldMarker -> EditorUtils.intersects(oldMarker, range)); 46 | completedTypeChanges.removeIf(typeChange -> EditorUtils.intersects(typeChange.newRangeMarker, range)); 47 | } 48 | 49 | public void clear() { 50 | uncompletedTypeChanges.clear(); 51 | completedTypeChanges.clear(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/refactoring/listeners/AfterTypeChangeCaretListener.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.refactoring.listeners; 2 | 3 | import com.intellij.openapi.editor.event.CaretEvent; 4 | import com.intellij.openapi.editor.event.CaretListener; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.research.ddtm.ide.refactoring.ReactiveTypeChangeAvailabilityUpdater; 7 | 8 | public class AfterTypeChangeCaretListener implements CaretListener { 9 | 10 | @Override 11 | public void caretPositionChanged(@NotNull CaretEvent event) { 12 | final var editor = event.getEditor(); 13 | final var caret = event.getCaret(); 14 | final var project = editor.getProject(); 15 | if (project == null || caret == null) return; 16 | 17 | final int offset = caret.getOffset(); 18 | final var updater = project.getService(ReactiveTypeChangeAvailabilityUpdater.class); 19 | updater.updateHighlighter(editor, offset); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/refactoring/listeners/RenameRefactoringEventListener.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.refactoring.listeners; 2 | 3 | import com.intellij.psi.PsiElement; 4 | import com.intellij.refactoring.listeners.RefactoringEventData; 5 | import com.intellij.refactoring.listeners.RefactoringEventListener; 6 | import org.jetbrains.annotations.NotNull; 7 | import org.jetbrains.annotations.Nullable; 8 | import org.jetbrains.research.ddtm.ide.fus.TypeChangeLogsCollector; 9 | 10 | public class RenameRefactoringEventListener implements RefactoringEventListener { 11 | @Override 12 | public void refactoringStarted(@NotNull String refactoringId, @Nullable RefactoringEventData beforeData) { 13 | } 14 | 15 | @Override 16 | public void refactoringDone(@NotNull String refactoringId, @Nullable RefactoringEventData afterData) { 17 | if (afterData == null) return; 18 | if (refactoringId.contains("rename")) { 19 | final var elementKey = afterData.get().getKeys()[0]; 20 | final PsiElement element = (PsiElement) afterData.get().get(elementKey); 21 | if (element == null) return; 22 | TypeChangeLogsCollector.getInstance().renamePerformed(element.getProject(), element.getClass().getCanonicalName()); 23 | } 24 | } 25 | 26 | @Override 27 | public void conflictsDetected(@NotNull String refactoringId, @NotNull RefactoringEventData conflictsData) { 28 | 29 | } 30 | 31 | @Override 32 | public void undoRefactoring(@NotNull String refactoringId) { 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/refactoring/listeners/TypeChangeDocumentListener.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.refactoring.listeners; 2 | 3 | import com.intellij.ide.DataManager; 4 | import com.intellij.openapi.actionSystem.PlatformDataKeys; 5 | import com.intellij.openapi.command.undo.UndoManager; 6 | import com.intellij.openapi.components.Service; 7 | import com.intellij.openapi.diagnostic.Logger; 8 | import com.intellij.openapi.editor.Document; 9 | import com.intellij.openapi.editor.RangeMarker; 10 | import com.intellij.openapi.editor.event.DocumentEvent; 11 | import com.intellij.openapi.editor.event.DocumentListener; 12 | import com.intellij.openapi.project.IndexNotReadyException; 13 | import com.intellij.openapi.project.Project; 14 | import com.intellij.openapi.util.TextRange; 15 | import com.intellij.psi.PsiDocumentManager; 16 | import com.intellij.psi.PsiElement; 17 | import com.intellij.psi.PsiFile; 18 | import com.intellij.psi.PsiTypeElement; 19 | import com.intellij.util.IncorrectOperationException; 20 | import com.intellij.util.SlowOperations; 21 | import org.jetbrains.annotations.NotNull; 22 | import org.jetbrains.research.ddtm.data.TypeChangeRulesStorage; 23 | import org.jetbrains.research.ddtm.ide.refactoring.ReactiveTypeChangeAvailabilityUpdater; 24 | import org.jetbrains.research.ddtm.ide.refactoring.services.TypeChangeRefactoringProvider; 25 | import org.jetbrains.research.ddtm.ide.settings.TypeChangeSettingsState; 26 | import org.jetbrains.research.ddtm.utils.PsiRelatedUtils; 27 | 28 | import java.util.concurrent.ExecutionException; 29 | import java.util.concurrent.TimeoutException; 30 | 31 | @Service 32 | public final class TypeChangeDocumentListener implements DocumentListener { 33 | private static final Logger LOG = Logger.getInstance(TypeChangeDocumentListener.class); 34 | 35 | private final Project project; 36 | 37 | public TypeChangeDocumentListener(Project project) { 38 | this.project = project; 39 | } 40 | 41 | @Override 42 | public void beforeDocumentChange(@NotNull DocumentEvent event) { 43 | if (UndoManager.getInstance(project).isUndoInProgress()) return; 44 | 45 | final var state = TypeChangeRefactoringProvider.getInstance(project).getState(); 46 | if (state.isInternalTypeChangeInProgress) return; 47 | 48 | final var document = event.getDocument(); 49 | final var psiDocumentManager = PsiDocumentManager.getInstance(project); 50 | if (!psiDocumentManager.isCommitted(document) || psiDocumentManager.isDocumentBlockedByPsi(document)) return; 51 | 52 | var psiFile = PsiDocumentManager.getInstance(project).getCachedPsiFile(document); 53 | if (psiFile == null || PsiRelatedUtils.shouldIgnoreFile(psiFile)) return; 54 | 55 | final int offset = event.getOffset(); 56 | try { 57 | for (var rangeMarker : state.uncompletedTypeChanges.keySet()) { 58 | if (document.getLineNumber(rangeMarker.getStartOffset()) == document.getLineNumber(offset)) return; 59 | } 60 | } catch (IndexOutOfBoundsException ex) { 61 | LOG.warn("Wrong offset"); 62 | state.uncompletedTypeChanges.clear(); 63 | return; 64 | } 65 | 66 | final var oldElement = psiFile.findElementAt(offset); 67 | if (oldElement == null) return; 68 | 69 | final PsiTypeElement oldTypeElement = PsiRelatedUtils.getClosestPsiTypeElement(oldElement); 70 | if (oldTypeElement == null) return; 71 | String sourceType; 72 | try { 73 | sourceType = oldTypeElement.getType().getCanonicalText(); 74 | final var storage = project.getService(TypeChangeRulesStorage.class); 75 | if (storage.hasSourceType(sourceType)) { 76 | processSourceTypeChangeEvent(oldElement, sourceType, document); 77 | } 78 | } catch (IndexNotReadyException | IncorrectOperationException e) { 79 | LOG.warn(e); 80 | } 81 | } 82 | 83 | @Override 84 | public void documentChanged(@NotNull DocumentEvent event) { 85 | if (UndoManager.getInstance(project).isUndoInProgress()) return; 86 | 87 | final var state = TypeChangeRefactoringProvider.getInstance(project).getState(); 88 | if (state.isInternalTypeChangeInProgress) return; 89 | final var document = event.getDocument(); 90 | 91 | PsiDocumentManager.getInstance(project).performForCommittedDocument(document, new Runnable() { 92 | final DocumentEvent e = event; 93 | 94 | @Override 95 | public void run() { 96 | try { 97 | SlowOperations.allowSlowOperations(() -> documentChangedAndCommitted(e)); 98 | } catch (Exception executionException) { 99 | LOG.warn(executionException); 100 | } 101 | } 102 | }); 103 | } 104 | 105 | private void documentChangedAndCommitted(DocumentEvent event) throws ExecutionException, TimeoutException { 106 | // FIXME: if I keep a reference to the Project in the field, then it throws an exception "Light files should have only one PSI" 107 | // But the similar approach is working in Suggested Refactoring... 108 | final var dataContext = DataManager.getInstance().getDataContextFromFocusAsync().blockingGet(500); 109 | if (dataContext == null) return; 110 | 111 | final var project = dataContext.getData(PlatformDataKeys.PROJECT); 112 | if (project == null || project.isDisposed()) return; 113 | 114 | final Document document = event.getDocument(); 115 | PsiFile psiFile = null; 116 | try { 117 | psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document); 118 | } catch (Throwable e) { 119 | LOG.warn(e); 120 | } 121 | if (psiFile == null || PsiRelatedUtils.shouldIgnoreFile(psiFile)) return; 122 | 123 | final int offset = event.getOffset(); 124 | PsiElement newElement = psiFile.findElementAt(offset); 125 | if (newElement == null) return; 126 | 127 | final PsiTypeElement newTypeElement = PsiRelatedUtils.getClosestPsiTypeElement(newElement); 128 | if (newTypeElement == null) return; 129 | final String fqTargetType = newTypeElement.getType().getCanonicalText(); 130 | 131 | final var storage = project.getService(TypeChangeRulesStorage.class); 132 | if (storage.hasTargetType(fqTargetType)) { 133 | processTargetTypeChangeEvent(newTypeElement, fqTargetType, document); 134 | 135 | final var updater = project.getService(ReactiveTypeChangeAvailabilityUpdater.class); 136 | updater.updateAllHighlighters(event.getDocument(), newTypeElement.getTextRange().getEndOffset()); 137 | } 138 | } 139 | 140 | private void processSourceTypeChangeEvent(PsiElement oldElement, String sourceType, Document document) { 141 | final TextRange range = TextRange.from( 142 | oldElement.getTextOffset(), 143 | oldElement.getTextLength() 144 | ); 145 | final RangeMarker rangeMarker = document.createRangeMarker(range); 146 | 147 | final var state = TypeChangeRefactoringProvider.getInstance(project).getState(); 148 | state.uncompletedTypeChanges.put(rangeMarker, sourceType); 149 | } 150 | 151 | private void processTargetTypeChangeEvent(PsiTypeElement newTypeElement, String targetType, Document document) { 152 | final var newRange = TextRange.from( 153 | newTypeElement.getTextOffset(), 154 | newTypeElement.getTextLength() + 1 155 | ); 156 | final var state = TypeChangeRefactoringProvider.getInstance(project).getState(); 157 | 158 | RangeMarker relevantOldRangeMarker = null; 159 | String relevantSourceType = null; 160 | for (var entry : state.uncompletedTypeChanges.entrySet()) { 161 | final RangeMarker oldRangeMarker = entry.getKey(); 162 | final String sourceType = entry.getValue(); 163 | if (oldRangeMarker.getDocument() != document) continue; 164 | 165 | final var storage = project.getService(TypeChangeRulesStorage.class); 166 | final var oldRange = new TextRange(oldRangeMarker.getStartOffset(), oldRangeMarker.getEndOffset()); 167 | if (oldRange.intersects(newRange) && storage.findPattern(sourceType, targetType).isPresent()) { 168 | relevantOldRangeMarker = oldRangeMarker; 169 | relevantSourceType = sourceType; 170 | break; 171 | } 172 | } 173 | if (relevantOldRangeMarker == null || relevantSourceType == null) return; 174 | state.uncompletedTypeChanges.remove(relevantOldRangeMarker); 175 | 176 | final var newRangeMarker = document.createRangeMarker(newRange); 177 | state.addCompletedTypeChange(relevantOldRangeMarker, newRangeMarker, relevantSourceType, targetType); 178 | state.refactoringEnabled = true; 179 | 180 | Thread disablerWaitThread = new Thread(() -> { 181 | try { 182 | Thread.sleep(TypeChangeSettingsState.getInstance().disableIntentionTimeout); 183 | state.refactoringEnabled = false; 184 | state.completedTypeChanges.clear(); 185 | state.uncompletedTypeChanges.clear(); 186 | } catch (InterruptedException e) { 187 | LOG.error(e); 188 | } 189 | }); 190 | disablerWaitThread.start(); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/refactoring/listeners/UndoTypeChangeListener.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.refactoring.listeners; 2 | 3 | import com.intellij.openapi.command.CommandEvent; 4 | import com.intellij.openapi.command.CommandListener; 5 | import org.jetbrains.annotations.NotNull; 6 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 7 | import org.jetbrains.research.ddtm.data.TypeChangeRulesStorage; 8 | import org.jetbrains.research.ddtm.ide.fus.TypeChangeLogsCollector; 9 | 10 | public class UndoTypeChangeListener implements CommandListener { 11 | @Override 12 | public void commandStarted(@NotNull CommandEvent event) { 13 | if (event.getProject() == null || event.getCommandName() == null) return; 14 | String expectedCommandNamePrefix = DataDrivenTypeMigrationBundle.message("group.id"); 15 | if (event.getCommandName().startsWith("Undo " + expectedCommandNamePrefix)) { 16 | String[] typePair = event.getCommandName().substring(7 + expectedCommandNamePrefix.length()).split(" to "); 17 | final var pattern = event.getProject().getService(TypeChangeRulesStorage.class).findPattern(typePair[0], typePair[1]); 18 | if (pattern.isEmpty()) return; 19 | TypeChangeLogsCollector.getInstance().migrationUndone(event.getProject(), pattern.get().getId()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/refactoring/services/TypeChangeIntentionContributor.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.refactoring.services; 2 | 3 | import com.intellij.codeInsight.daemon.impl.HighlightInfo; 4 | import com.intellij.codeInsight.daemon.impl.IntentionMenuContributor; 5 | import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass; 6 | import com.intellij.codeInsight.intention.IntentionAction; 7 | import com.intellij.icons.AllIcons; 8 | import com.intellij.openapi.editor.Editor; 9 | import com.intellij.psi.PsiElement; 10 | import com.intellij.psi.PsiFile; 11 | import org.jetbrains.annotations.NotNull; 12 | import org.jetbrains.research.ddtm.data.models.TypeChangeRuleDescriptor; 13 | import org.jetbrains.research.ddtm.ide.intentions.FailedTypeChangeRecoveringIntention; 14 | import org.jetbrains.research.ddtm.ide.intentions.SuggestedTypeChangeIntention; 15 | import org.jetbrains.research.ddtm.ide.migration.collectors.TypeChangesInfoCollector; 16 | import org.jetbrains.research.ddtm.ide.refactoring.TypeChangeMarker; 17 | 18 | import javax.swing.*; 19 | import java.util.Objects; 20 | import java.util.Optional; 21 | 22 | class TypeChangeIntentionContributor implements IntentionMenuContributor { 23 | private final Icon icon = AllIcons.Actions.SuggestedRefactoringBulb; 24 | 25 | @Override 26 | public void collectActions(@NotNull Editor hostEditor, 27 | @NotNull PsiFile hostFile, 28 | @NotNull ShowIntentionsPass.IntentionsInfo intentions, 29 | int passIdToShowIntentionsFor, 30 | int offset) { 31 | final PsiElement context = hostFile.findElementAt(offset); 32 | if (context == null) return; 33 | final var failedTypeChangesCollector = TypeChangesInfoCollector.getInstance(); 34 | final Optional rule = failedTypeChangesCollector.getRuleForFailedUsage(context); 35 | 36 | IntentionAction intention; 37 | if (rule.isPresent()) { 38 | intention = new FailedTypeChangeRecoveringIntention(rule.get()); 39 | } else { 40 | final var state = TypeChangeRefactoringProvider.getInstance(hostEditor.getProject()).getState(); 41 | final var typeChangeMarker = state.getCompletedTypeChangeForOffset(offset); 42 | if (typeChangeMarker.isEmpty() || !state.refactoringEnabled) return; 43 | TypeChangeMarker marker = typeChangeMarker.get(); 44 | if (Objects.equals(marker.sourceType, marker.targetType)) 45 | return; // to prevent strange bugs with reactive intention 46 | 47 | intention = new SuggestedTypeChangeIntention(marker); 48 | } 49 | 50 | // we add it into 'errorFixesToShow' if it's not empty to always be at the top of the list 51 | // we don't add into it if it's empty to keep the color of the bulb 52 | final var collectionToAdd = 53 | (intentions.errorFixesToShow == null || intentions.errorFixesToShow.isEmpty()) 54 | ? intentions.inspectionFixesToShow 55 | : intentions.errorFixesToShow; 56 | collectionToAdd.add(0, new HighlightInfo.IntentionActionDescriptor( 57 | intention, null, null, icon, null, null, null) 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/refactoring/services/TypeChangeRefactoringProvider.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.refactoring.services; 2 | 3 | import com.intellij.concurrency.JobScheduler; 4 | import com.intellij.openapi.application.ApplicationManager; 5 | import com.intellij.openapi.diagnostic.Logger; 6 | import com.intellij.openapi.editor.EditorFactory; 7 | import com.intellij.openapi.editor.ProjectDisposeAwareDocumentListener; 8 | import com.intellij.openapi.editor.event.EditorFactoryEvent; 9 | import com.intellij.openapi.editor.event.EditorFactoryListener; 10 | import com.intellij.openapi.project.Project; 11 | import com.intellij.openapi.startup.StartupActivity; 12 | import org.jetbrains.annotations.NotNull; 13 | import org.jetbrains.research.ddtm.Config; 14 | import org.jetbrains.research.ddtm.ide.fus.TypeChangeLogsCollector; 15 | import org.jetbrains.research.ddtm.ide.refactoring.TypeChangeSuggestedRefactoringState; 16 | import org.jetbrains.research.ddtm.ide.refactoring.listeners.AfterTypeChangeCaretListener; 17 | import org.jetbrains.research.ddtm.ide.refactoring.listeners.TypeChangeDocumentListener; 18 | import org.jetbrains.research.ddtm.ide.settings.TypeChangeSettingsState; 19 | 20 | import java.util.Arrays; 21 | import java.util.concurrent.TimeUnit; 22 | 23 | public class TypeChangeRefactoringProvider { 24 | private final TypeChangeSuggestedRefactoringState state = new TypeChangeSuggestedRefactoringState(); 25 | public Project myProject; 26 | 27 | public TypeChangeRefactoringProvider(Project project) { 28 | this.myProject = project; 29 | } 30 | 31 | public static TypeChangeRefactoringProvider getInstance(Project project) { 32 | return project.getService(TypeChangeRefactoringProvider.class); 33 | } 34 | 35 | public TypeChangeSuggestedRefactoringState getState() { 36 | return state; 37 | } 38 | 39 | static class Startup implements StartupActivity.Background { 40 | private static final Logger LOG = Logger.getInstance(Startup.class); 41 | 42 | @Override 43 | public void runActivity(@NotNull Project project) { 44 | if (!ApplicationManager.getApplication().isUnitTestMode()) { 45 | TypeChangeLogsCollector.getInstance(); // logging baseline 46 | 47 | EditorFactory.getInstance().getEventMulticaster().addDocumentListener( 48 | ProjectDisposeAwareDocumentListener.create(project, project.getService(TypeChangeDocumentListener.class)), 49 | project 50 | ); 51 | 52 | Arrays.stream(EditorFactory.getInstance().getAllEditors()).forEach(editor -> 53 | editor.getCaretModel().addCaretListener(new AfterTypeChangeCaretListener())); 54 | 55 | EditorFactory.getInstance().addEditorFactoryListener(new EditorFactoryListener() { 56 | @Override 57 | public void editorCreated(@NotNull EditorFactoryEvent event) { 58 | event.getEditor().getCaretModel().addCaretListener(new AfterTypeChangeCaretListener()); 59 | } 60 | 61 | @Override 62 | public void editorReleased(@NotNull EditorFactoryEvent event) { 63 | event.getEditor().getCaretModel().removeCaretListener(new AfterTypeChangeCaretListener()); 64 | } 65 | }, project); 66 | 67 | TypeChangeSuggestedRefactoringState state = TypeChangeRefactoringProvider.getInstance(project).getState(); 68 | final long delay = Config.GARBAGE_COLLECTOR_FACTOR * TypeChangeSettingsState.getInstance().disableIntentionTimeout; 69 | JobScheduler.getScheduler().scheduleWithFixedDelay( 70 | state::clear, 71 | delay, 72 | delay, 73 | TimeUnit.MILLISECONDS 74 | ); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/settings/TypeChangeSettingsComponent.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.settings; 2 | 3 | import com.intellij.openapi.ui.ComboBox; 4 | import com.intellij.ui.JBIntSpinner; 5 | import com.intellij.ui.components.JBLabel; 6 | import com.intellij.util.ui.FormBuilder; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.research.ddtm.Config; 9 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 10 | import org.jetbrains.research.ddtm.data.enums.SupportedSearchScope; 11 | 12 | import javax.swing.*; 13 | 14 | public class TypeChangeSettingsComponent { 15 | private final JPanel panel; 16 | private final JBIntSpinner disableIntentionTimeoutIntSpinner = 17 | new JBIntSpinner(Config.DISABLE_INTENTION_TIMEOUT_BY_DEFAULT, 1_000, 100_000, 1_000); 18 | private final ComboBox searchScopeOptionsComboBox = 19 | new ComboBox<>(DataDrivenTypeMigrationBundle.SEARCH_SCOPE_OPTIONS.values().toArray(String[]::new)); 20 | 21 | public TypeChangeSettingsComponent() { 22 | panel = FormBuilder.createFormBuilder() 23 | .addLabeledComponent( 24 | new JBLabel(DataDrivenTypeMigrationBundle.message("settings.timeout.spinner.label")), 25 | disableIntentionTimeoutIntSpinner, 26 | 1, 27 | false 28 | ) 29 | .addLabeledComponent( 30 | new JBLabel(DataDrivenTypeMigrationBundle.message("settings.scope.combobox.label")), 31 | searchScopeOptionsComboBox, 32 | 1, 33 | false 34 | ) 35 | .addComponentFillVertically(new JPanel(), 0) 36 | .getPanel(); 37 | } 38 | 39 | public JPanel getPanel() { 40 | return panel; 41 | } 42 | 43 | public JComponent getPreferredFocusedComponent() { 44 | return searchScopeOptionsComboBox; 45 | } 46 | 47 | public SupportedSearchScope getSearchScopeOption() { 48 | return DataDrivenTypeMigrationBundle.SEARCH_SCOPE_OPTIONS.inverse().get(searchScopeOptionsComboBox.getSelectedItem()); 49 | } 50 | 51 | public void setSearchScopeOption(@NotNull SupportedSearchScope searchScope) { 52 | searchScopeOptionsComboBox.setSelectedItem(DataDrivenTypeMigrationBundle.SEARCH_SCOPE_OPTIONS.get(searchScope)); 53 | } 54 | 55 | public int getDisableIntentionTimeout() { 56 | return disableIntentionTimeoutIntSpinner.getNumber(); 57 | } 58 | 59 | public void setDisableIntentionTimeout(int timeout) { 60 | disableIntentionTimeoutIntSpinner.setNumber(timeout); 61 | } 62 | } -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/settings/TypeChangeSettingsConfigurable.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.settings; 2 | 3 | import com.intellij.openapi.options.Configurable; 4 | import org.jetbrains.annotations.Nls; 5 | import org.jetbrains.annotations.Nullable; 6 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 7 | 8 | import javax.swing.*; 9 | 10 | /** 11 | * Provides controller functionality for application settings. 12 | */ 13 | public class TypeChangeSettingsConfigurable implements Configurable { 14 | private TypeChangeSettingsComponent settingsComponent; 15 | 16 | @Nls(capitalization = Nls.Capitalization.Title) 17 | @Override 18 | public String getDisplayName() { 19 | return DataDrivenTypeMigrationBundle.message("settings.display.name"); 20 | } 21 | 22 | @Override 23 | public JComponent getPreferredFocusedComponent() { 24 | return settingsComponent.getPreferredFocusedComponent(); 25 | } 26 | 27 | @Nullable 28 | @Override 29 | public JComponent createComponent() { 30 | settingsComponent = new TypeChangeSettingsComponent(); 31 | return settingsComponent.getPanel(); 32 | } 33 | 34 | @Override 35 | public boolean isModified() { 36 | TypeChangeSettingsState settings = TypeChangeSettingsState.getInstance(); 37 | return !settingsComponent.getSearchScopeOption().equals(settings.searchScope) 38 | || settingsComponent.getDisableIntentionTimeout() != settings.disableIntentionTimeout; 39 | } 40 | 41 | @Override 42 | public void apply() { 43 | TypeChangeSettingsState settings = TypeChangeSettingsState.getInstance(); 44 | settings.searchScope = settingsComponent.getSearchScopeOption(); 45 | settings.disableIntentionTimeout = settingsComponent.getDisableIntentionTimeout(); 46 | } 47 | 48 | @Override 49 | public void reset() { 50 | TypeChangeSettingsState settings = TypeChangeSettingsState.getInstance(); 51 | settingsComponent.setSearchScopeOption(settings.searchScope); 52 | settingsComponent.setDisableIntentionTimeout(settings.disableIntentionTimeout); 53 | } 54 | 55 | @Override 56 | public void disposeUIResources() { 57 | settingsComponent = null; 58 | } 59 | } -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/settings/TypeChangeSettingsState.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.settings; 2 | 3 | import com.intellij.openapi.application.ApplicationManager; 4 | import com.intellij.openapi.components.PersistentStateComponent; 5 | import com.intellij.openapi.components.State; 6 | import com.intellij.openapi.components.Storage; 7 | import com.intellij.util.xmlb.XmlSerializerUtil; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.jetbrains.annotations.Nullable; 10 | import org.jetbrains.research.ddtm.Config; 11 | import org.jetbrains.research.ddtm.data.enums.SupportedSearchScope; 12 | 13 | /** 14 | * Supports storing the application settings in a persistent way. 15 | * The {@link State} and {@link Storage} annotations define the name of the data and the file name where 16 | * these persistent application settings are stored. 17 | */ 18 | @State( 19 | name = "TypeChangeSettingsState", 20 | storages = {@Storage("TypeChangeSettingsPlugin.xml")} 21 | ) 22 | public class TypeChangeSettingsState implements PersistentStateComponent { 23 | public SupportedSearchScope searchScope = SupportedSearchScope.FILE; 24 | public int disableIntentionTimeout = Config.DISABLE_INTENTION_TIMEOUT_BY_DEFAULT; 25 | 26 | public static TypeChangeSettingsState getInstance() { 27 | return ApplicationManager.getApplication().getService(TypeChangeSettingsState.class); 28 | } 29 | 30 | @Nullable 31 | @Override 32 | public TypeChangeSettingsState getState() { 33 | return this; 34 | } 35 | 36 | @Override 37 | public void loadState(@NotNull TypeChangeSettingsState state) { 38 | XmlSerializerUtil.copyBean(state, this); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/ui/FailedTypeChangesPanel.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.ui; 2 | 3 | import com.intellij.openapi.Disposable; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.openapi.util.Disposer; 6 | import com.intellij.psi.PsiElement; 7 | import com.intellij.ui.content.Content; 8 | import com.intellij.usageView.UsageInfo; 9 | 10 | import javax.swing.*; 11 | import java.awt.*; 12 | 13 | public class FailedTypeChangesPanel extends JPanel implements Disposable { 14 | private final FailedTypeChangesUsagesPanel usagesPanel; 15 | 16 | public FailedTypeChangesPanel(Project project) { 17 | super(new BorderLayout()); 18 | usagesPanel = new FailedTypeChangesUsagesPanel(project); 19 | Disposer.register(this, usagesPanel); 20 | add(usagesPanel, BorderLayout.CENTER); 21 | } 22 | 23 | public void setContent(final Content content) { 24 | Disposer.register(content, this); 25 | } 26 | 27 | public void updateLayout(UsageInfo[] infos) { 28 | usagesPanel.showUsages(PsiElement.EMPTY_ARRAY, infos); 29 | } 30 | 31 | @Override 32 | public void dispose() { 33 | Disposer.dispose(usagesPanel); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/ui/FailedTypeChangesUsagesPanel.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.ui; 2 | 3 | import com.intellij.codeInsight.CodeInsightBundle; 4 | import com.intellij.openapi.Disposable; 5 | import com.intellij.openapi.actionSystem.DataProvider; 6 | import com.intellij.openapi.actionSystem.PlatformDataKeys; 7 | import com.intellij.openapi.application.AppUIExecutor; 8 | import com.intellij.openapi.application.ModalityState; 9 | import com.intellij.openapi.progress.ProcessCanceledException; 10 | import com.intellij.openapi.progress.ProgressIndicator; 11 | import com.intellij.openapi.project.Project; 12 | import com.intellij.openapi.util.Disposer; 13 | import com.intellij.packageDependencies.ui.UsagesPanel; 14 | import com.intellij.psi.PsiElement; 15 | import com.intellij.usageView.UsageInfo; 16 | import com.intellij.usages.*; 17 | import com.intellij.usages.impl.UsageViewImpl; 18 | import com.intellij.util.Alarm; 19 | import org.jetbrains.annotations.Nls; 20 | import org.jetbrains.annotations.NonNls; 21 | import org.jetbrains.annotations.NotNull; 22 | import org.jetbrains.annotations.Nullable; 23 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 24 | 25 | import javax.swing.*; 26 | import java.awt.*; 27 | 28 | /** 29 | * This class is mostly copied from {@link UsagesPanel}, but it fixes the problem with JTree expansion 30 | * in {@link FailedTypeChangesUsagesPanel#showUsages} method. 31 | */ 32 | public class FailedTypeChangesUsagesPanel extends JPanel implements Disposable, DataProvider { 33 | protected final Alarm myAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD); 34 | private final Project myProject; 35 | ProgressIndicator myCurrentProgress; 36 | private JComponent myCurrentComponent; 37 | private UsageView myCurrentUsageView; 38 | 39 | public FailedTypeChangesUsagesPanel(Project project) { 40 | super(new BorderLayout()); 41 | myProject = project; 42 | setToInitialPosition(); 43 | } 44 | 45 | private static JComponent createLabel(@Nls String text) { 46 | JLabel label = new JLabel(text); 47 | label.setHorizontalAlignment(SwingConstants.CENTER); 48 | return label; 49 | } 50 | 51 | public void setToInitialPosition() { 52 | cancelCurrentFindRequest(); 53 | setToComponent(createLabel(getInitialPositionText())); 54 | } 55 | 56 | public @Nls String getInitialPositionText() { 57 | return DataDrivenTypeMigrationBundle.message("tool.window.initial.position.text"); 58 | } 59 | 60 | public @Nls String getCodeUsagesString() { 61 | return DataDrivenTypeMigrationBundle.message("tool.window.code.usages.string"); 62 | } 63 | 64 | void cancelCurrentFindRequest() { 65 | if (myCurrentProgress != null) { 66 | myCurrentProgress.cancel(); 67 | } 68 | } 69 | 70 | protected void showUsages(PsiElement @NotNull [] primaryElements, UsageInfo @NotNull [] usageInfos) { 71 | if (myCurrentUsageView != null) { 72 | Disposer.dispose(myCurrentUsageView); 73 | } 74 | try { 75 | Usage[] usages = UsageInfoToUsageConverter.convert(primaryElements, usageInfos); 76 | UsageViewPresentation presentation = new UsageViewPresentation(); 77 | presentation.setCodeUsagesString(getCodeUsagesString()); 78 | myCurrentUsageView = UsageViewManager.getInstance(myProject).createUsageView(UsageTarget.EMPTY_ARRAY, usages, presentation, null); 79 | setToComponent(myCurrentUsageView.getComponent()); 80 | if (!((UsageViewImpl) myCurrentUsageView).isDisposed()) { 81 | ((UsageViewImpl) myCurrentUsageView).expandAll(); 82 | } 83 | } catch (ProcessCanceledException e) { 84 | setToCanceled(); 85 | } 86 | } 87 | 88 | private void setToCanceled() { 89 | setToComponent(createLabel(CodeInsightBundle.message("usage.view.canceled"))); 90 | } 91 | 92 | final void setToComponent(@NotNull JComponent component) { 93 | AppUIExecutor.onWriteThread(ModalityState.any()).expireWith(myProject).execute(() -> { 94 | if (myCurrentComponent != null) { 95 | if (myCurrentUsageView != null && myCurrentComponent == myCurrentUsageView.getComponent()) { 96 | Disposer.dispose(myCurrentUsageView); 97 | myCurrentUsageView = null; 98 | } 99 | remove(myCurrentComponent); 100 | } 101 | myCurrentComponent = component; 102 | add(component, BorderLayout.CENTER); 103 | revalidate(); 104 | }); 105 | } 106 | 107 | @Override 108 | public void dispose() { 109 | if (myCurrentUsageView != null) { 110 | Disposer.dispose(myCurrentUsageView); 111 | myCurrentUsageView = null; 112 | } 113 | } 114 | 115 | @Override 116 | @Nullable 117 | @NonNls 118 | public Object getData(@NotNull @NonNls String dataId) { 119 | if (PlatformDataKeys.HELP_ID.is(dataId)) { 120 | return "ideaInterface.find"; 121 | } 122 | return null; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/ui/TypeChangeClassicModePanel.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.ui; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.openapi.ui.ComboBox; 5 | import com.intellij.psi.PsiType; 6 | import com.intellij.ui.components.JBLabel; 7 | import com.intellij.util.ui.FormBuilder; 8 | import com.intellij.util.ui.JBUI; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 11 | import org.jetbrains.research.ddtm.data.enums.SupportedSearchScope; 12 | import org.jetbrains.research.ddtm.data.models.TypeChangePatternDescriptor; 13 | import org.jetbrains.research.ddtm.ide.settings.TypeChangeSettingsState; 14 | import org.jetbrains.research.ddtm.utils.StringUtils; 15 | 16 | import javax.swing.*; 17 | import java.awt.*; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.function.Function; 21 | import java.util.stream.Collectors; 22 | 23 | import static org.apache.commons.lang.StringEscapeUtils.escapeHtml; 24 | 25 | public class TypeChangeClassicModePanel extends JPanel { 26 | private final ComboBox searchScopeOptionsComboBox = 27 | new ComboBox<>(DataDrivenTypeMigrationBundle.SEARCH_SCOPE_OPTIONS.values().toArray(String[]::new)); 28 | private final ComboBox typeMigrationRulesComboBox; 29 | private final Map textToPattern; 30 | 31 | public TypeChangeClassicModePanel(List rulesDescriptors, PsiType sourceType, Project project) { 32 | super.setLayout(new BorderLayout()); 33 | 34 | textToPattern = rulesDescriptors.stream().collect(Collectors.toMap( 35 | pattern -> StringUtils.escapeSSRTemplates(pattern.resolveTargetType(sourceType, project)), 36 | Function.identity() 37 | )); 38 | typeMigrationRulesComboBox = new ComboBox<>(textToPattern.keySet().toArray(String[]::new)); 39 | typeMigrationRulesComboBox.setMinimumAndPreferredWidth(500); 40 | 41 | final JPanel panel = FormBuilder.createFormBuilder() 42 | .setHorizontalGap(10) 43 | .addLabeledComponent( 44 | new JBLabel(DataDrivenTypeMigrationBundle.message("dialog.migration.rules.combobox.label", 45 | escapeHtml(StringUtils.escapeSSRTemplates(sourceType.getCanonicalText())))), 46 | typeMigrationRulesComboBox, 47 | 5, 48 | true 49 | ) 50 | .addComponentFillVertically(new JPanel(), 0) 51 | .addLabeledComponent( 52 | new JBLabel(DataDrivenTypeMigrationBundle.message("settings.scope.combobox.label")), 53 | searchScopeOptionsComboBox, 54 | 5, 55 | true 56 | ) 57 | .addComponentFillVertically(new JPanel(), 0) 58 | .getPanel(); 59 | 60 | add(panel); 61 | this.setSearchScopeOption(TypeChangeSettingsState.getInstance().searchScope); 62 | this.setBorder(JBUI.Borders.empty(5, 2)); 63 | } 64 | 65 | public TypeChangePatternDescriptor getSelectedPatternDescriptor() { 66 | return textToPattern.get(typeMigrationRulesComboBox.getSelectedItem()); 67 | } 68 | 69 | public SupportedSearchScope getSearchScopeOption() { 70 | return DataDrivenTypeMigrationBundle.SEARCH_SCOPE_OPTIONS.inverse().get(searchScopeOptionsComboBox.getSelectedItem()); 71 | } 72 | 73 | public void setSearchScopeOption(@NotNull SupportedSearchScope searchScope) { 74 | searchScopeOptionsComboBox.setSelectedItem(DataDrivenTypeMigrationBundle.SEARCH_SCOPE_OPTIONS.get(searchScope)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/ui/TypeChangeGutterPopupPanel.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.ui; 2 | 3 | import com.intellij.openapi.ui.ComboBox; 4 | import com.intellij.ui.components.JBLabel; 5 | import com.intellij.util.ui.FormBuilder; 6 | import com.intellij.util.ui.JBUI; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 9 | import org.jetbrains.research.ddtm.data.enums.SupportedSearchScope; 10 | import org.jetbrains.research.ddtm.ide.settings.TypeChangeSettingsState; 11 | 12 | import javax.swing.*; 13 | import java.awt.*; 14 | 15 | import static org.apache.commons.lang.StringEscapeUtils.escapeHtml; 16 | import static org.jetbrains.research.ddtm.utils.StringUtils.escapeSSRTemplates; 17 | 18 | public class TypeChangeGutterPopupPanel extends JPanel { 19 | public Runnable onRefactor; 20 | 21 | private final ComboBox searchScopeOptionsComboBox = 22 | new ComboBox<>(DataDrivenTypeMigrationBundle.SEARCH_SCOPE_OPTIONS.values().toArray(String[]::new)); 23 | 24 | public TypeChangeGutterPopupPanel(String sourceType, String targetType) { 25 | super.setLayout(new BorderLayout()); 26 | 27 | final var buttonPanel = new JPanel(); 28 | buttonPanel.setLayout(new BorderLayout()); 29 | JButton button = new JButton(DataDrivenTypeMigrationBundle.message("suggested.gutter.popup.button")) { 30 | @Override 31 | public boolean isDefaultButton() { 32 | return true; 33 | } 34 | }; 35 | buttonPanel.add(button, BorderLayout.EAST); 36 | 37 | final var textPanel = new JPanel(); 38 | textPanel.setLayout(new BorderLayout()); 39 | final var header = new JLabel(DataDrivenTypeMigrationBundle.message("suggested.gutter.popup.header")); 40 | final var content = new JLabel(DataDrivenTypeMigrationBundle.message( 41 | "suggested.gutter.popup.content", 42 | escapeHtml(escapeSSRTemplates(sourceType)), 43 | escapeHtml(escapeSSRTemplates(targetType)) 44 | )); 45 | content.setBorder(JBUI.Borders.empty(15, 20, 25, 20)); 46 | textPanel.add(header, BorderLayout.NORTH); 47 | textPanel.add(content, BorderLayout.SOUTH); 48 | 49 | this.setSearchScopeOption(TypeChangeSettingsState.getInstance().searchScope); 50 | final var searchScopePanel = FormBuilder.createFormBuilder() 51 | .setHorizontalGap(30) 52 | .addLabeledComponent( 53 | new JBLabel(DataDrivenTypeMigrationBundle.message("suggested.gutter.popup.scope.combobox.label")), 54 | searchScopeOptionsComboBox, 55 | 1, 56 | false 57 | ) 58 | .getPanel(); 59 | searchScopePanel.setBorder(JBUI.Borders.emptyBottom(15)); 60 | 61 | add(textPanel, BorderLayout.NORTH); 62 | add(searchScopePanel, BorderLayout.CENTER); 63 | add(buttonPanel, BorderLayout.SOUTH); 64 | 65 | this.setBorder(JBUI.Borders.empty(5, 2)); 66 | button.addActionListener(actionEvent -> onRefactor.run()); 67 | } 68 | 69 | public SupportedSearchScope getSearchScopeOption() { 70 | return DataDrivenTypeMigrationBundle.SEARCH_SCOPE_OPTIONS.inverse().get(searchScopeOptionsComboBox.getSelectedItem()); 71 | } 72 | 73 | public void setSearchScopeOption(@NotNull SupportedSearchScope searchScope) { 74 | searchScopeOptionsComboBox.setSelectedItem(DataDrivenTypeMigrationBundle.SEARCH_SCOPE_OPTIONS.get(searchScope)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/ide/ui/TypeChangesListPopupStep.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide.ui; 2 | 3 | import com.intellij.openapi.progress.ProgressManager; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.openapi.ui.popup.PopupStep; 6 | import com.intellij.openapi.ui.popup.util.BaseListPopupStep; 7 | import com.intellij.psi.PsiElement; 8 | import com.intellij.psi.PsiType; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | import org.jetbrains.research.ddtm.DataDrivenTypeMigrationBundle; 12 | import org.jetbrains.research.ddtm.data.enums.InvocationWorkflow; 13 | import org.jetbrains.research.ddtm.data.models.TypeChangePatternDescriptor; 14 | import org.jetbrains.research.ddtm.ide.migration.TypeChangeProcessor; 15 | import org.jetbrains.research.ddtm.utils.PsiRelatedUtils; 16 | import org.jetbrains.research.ddtm.utils.StringUtils; 17 | 18 | import java.util.Comparator; 19 | import java.util.List; 20 | import java.util.Objects; 21 | import java.util.stream.Collectors; 22 | 23 | public class TypeChangesListPopupStep extends BaseListPopupStep { 24 | private final InvocationWorkflow invocationWorkflow; 25 | private final Project project; 26 | private final PsiElement context; 27 | private final PsiType sourceType; 28 | private TypeChangePatternDescriptor selectedPatternDescriptor = null; 29 | 30 | public TypeChangesListPopupStep(String caption, 31 | List rulesDescriptors, 32 | PsiElement context, 33 | Project project, 34 | InvocationWorkflow invocationWorkflow) { 35 | super(caption, rulesDescriptors.stream() 36 | .sorted(Comparator.comparing(TypeChangePatternDescriptor::getRank).reversed()) 37 | .collect(Collectors.toList()) 38 | ); 39 | this.sourceType = Objects.requireNonNull(PsiRelatedUtils.getClosestPsiTypeElement(context)).getType(); 40 | this.context = context; 41 | this.project = project; 42 | this.invocationWorkflow = invocationWorkflow; 43 | } 44 | 45 | @Override 46 | public @Nullable PopupStep onChosen(TypeChangePatternDescriptor selectedValue, boolean finalChoice) { 47 | selectedPatternDescriptor = selectedValue; 48 | return super.onChosen(selectedValue, finalChoice); 49 | } 50 | 51 | @Override 52 | public @NotNull String getTextFor(TypeChangePatternDescriptor value) { 53 | return DataDrivenTypeMigrationBundle.message( 54 | "intention.list.item.text", 55 | StringUtils.escapeSSRTemplates(value.resolveTargetType(sourceType, project)) 56 | ); 57 | } 58 | 59 | @Override 60 | public @Nullable Runnable getFinalRunnable() { 61 | return () -> { 62 | final var processor = new TypeChangeProcessor(project, invocationWorkflow); 63 | ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { 64 | ProgressManager.getInstance().getProgressIndicator().setIndeterminate(true); 65 | processor.run(context, selectedPatternDescriptor); 66 | }, DataDrivenTypeMigrationBundle.message("intention.family.name"), true, project); 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/legacy/CloningRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.legacy; 2 | 3 | import com.intellij.dvcs.repo.Repository; 4 | import com.intellij.dvcs.repo.VcsRepositoryManager; 5 | import com.intellij.openapi.progress.EmptyProgressIndicator; 6 | import com.intellij.openapi.progress.ProgressManager; 7 | import com.intellij.openapi.util.io.FileUtil; 8 | import com.intellij.openapi.vcs.ProjectLevelVcsManager; 9 | import com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl; 10 | import com.intellij.openapi.vcs.impl.VcsInitObject; 11 | import com.intellij.openapi.vfs.VfsUtil; 12 | import com.intellij.openapi.vfs.VirtualFile; 13 | import com.intellij.openapi.vfs.VirtualFileManager; 14 | import com.intellij.testFramework.HeavyPlatformTestCase; 15 | import git4idea.checkout.GitCheckoutProvider; 16 | import git4idea.commands.Git; 17 | 18 | import java.io.IOException; 19 | import java.nio.file.Path; 20 | import java.util.Collection; 21 | 22 | public class CloningRepositoryTest extends HeavyPlatformTestCase { 23 | final String url = "https://github.com/JetBrains-Research/data-driven-type-migration.git"; 24 | final String commitHashToCheckout = "9eaa67bb"; 25 | 26 | Path myTestNioRoot; 27 | 28 | private VirtualFile getMyProjectRoot() { 29 | return getOrCreateProjectBaseDir(); 30 | } 31 | 32 | @Override 33 | protected void setUp() throws Exception { 34 | super.setUp(); 35 | this.myTestNioRoot = getTempDir().createVirtualDir("test_root").toNioPath(); 36 | } 37 | 38 | public void testCloningProject() throws IOException, InterruptedException { 39 | final String projectName = url.substring(url.lastIndexOf('/') + 1).replace(".git", ""); 40 | final String parentDirectory = myTestNioRoot.toString(); 41 | final Git git = Git.getInstance(); 42 | 43 | final var indicator = new EmptyProgressIndicator(); 44 | ProgressManager.getInstance().executeProcessUnderProgress(() -> { 45 | assertTrue(GitCheckoutProvider.doClone(getProject(), git, projectName, parentDirectory, url)); 46 | }, indicator); 47 | 48 | VirtualFileManager.getInstance().refreshWithoutFileWatcher(false); 49 | VfsUtil.markDirtyAndRefresh(false, true, false, getMyProjectRoot()); 50 | 51 | ((ProjectLevelVcsManagerImpl) ProjectLevelVcsManager.getInstance(myProject)) 52 | .addInitializationRequest(VcsInitObject.AFTER_COMMON, () -> { 53 | Collection repositories = VcsRepositoryManager.getInstance(myProject).getRepositories(); 54 | System.out.println(repositories); 55 | }); 56 | 57 | FileUtil.delete(myTestNioRoot); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/legacy/PsiTypeChangeListener.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.legacy; 2 | 3 | import com.intellij.openapi.diagnostic.Logger; 4 | import com.intellij.openapi.editor.Document; 5 | import com.intellij.openapi.editor.RangeMarker; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.openapi.util.TextRange; 8 | import com.intellij.psi.*; 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.research.ddtm.data.TypeChangeRulesStorage; 11 | import org.jetbrains.research.ddtm.ide.refactoring.ReactiveTypeChangeAvailabilityUpdater; 12 | import org.jetbrains.research.ddtm.ide.refactoring.services.TypeChangeRefactoringProvider; 13 | import org.jetbrains.research.ddtm.ide.settings.TypeChangeSettingsState; 14 | import org.jetbrains.research.ddtm.utils.PsiRelatedUtils; 15 | 16 | public class PsiTypeChangeListener extends PsiTreeChangeAdapter { 17 | private static final Logger LOG = Logger.getInstance(PsiTypeChangeListener.class); 18 | 19 | private final Project project; 20 | private final TypeChangeRulesStorage storage; 21 | private final PsiDocumentManager documentManager; 22 | private final ReactiveTypeChangeAvailabilityUpdater updater; 23 | 24 | public PsiTypeChangeListener(Project project) { 25 | this.project = project; 26 | this.storage = project.getService(TypeChangeRulesStorage.class); 27 | this.updater = project.getService(ReactiveTypeChangeAvailabilityUpdater.class); 28 | this.documentManager = PsiDocumentManager.getInstance(project); 29 | } 30 | 31 | @Override 32 | public void beforeChildReplacement(@NotNull PsiTreeChangeEvent event) { 33 | super.beforeChildReplacement(event); 34 | final var state = TypeChangeRefactoringProvider.getInstance(project).getState(); 35 | if (state.isInternalTypeChangeInProgress) return; 36 | 37 | final var psiFile = event.getFile(); 38 | if (psiFile == null || PsiRelatedUtils.shouldIgnoreFile(psiFile)) return; 39 | 40 | final var document = documentManager.getDocument(psiFile); 41 | if (document == null) return; 42 | 43 | final var oldElement = event.getOldChild(); 44 | if (oldElement == null) return; 45 | 46 | final PsiTypeElement oldTypeElement = PsiRelatedUtils.getClosestPsiTypeElement(oldElement); 47 | if (oldTypeElement == null) return; 48 | final String sourceType = oldTypeElement.getType().getCanonicalText(); 49 | 50 | if (storage.hasSourceType(sourceType)) { 51 | processSourceTypeChangeEvent(oldElement, sourceType, document); 52 | } 53 | } 54 | 55 | @Override 56 | public void childReplaced(@NotNull PsiTreeChangeEvent event) { 57 | super.childReplaced(event); 58 | final var state = TypeChangeRefactoringProvider.getInstance(project).getState(); 59 | if (state.isInternalTypeChangeInProgress) return; 60 | 61 | final PsiFile psiFile = event.getFile(); 62 | if (psiFile == null || PsiRelatedUtils.shouldIgnoreFile(psiFile)) return; 63 | 64 | final var document = documentManager.getDocument(psiFile); 65 | if (document == null) return; 66 | 67 | PsiElement newElement = event.getNewChild(); 68 | if (newElement == null) return; 69 | 70 | final PsiTypeElement newTypeElement = PsiRelatedUtils.getClosestPsiTypeElement(newElement); 71 | if (newTypeElement == null) return; 72 | 73 | final String fqTargetType = newTypeElement.getType().getCanonicalText(); 74 | final String fqTargetTypeWithoutGenerics = 75 | fqTargetType.contains("<") 76 | ? fqTargetType.substring(0, fqTargetType.indexOf('<')) 77 | : fqTargetType; 78 | final String shortenedTargetType = fqTargetTypeWithoutGenerics.substring(fqTargetTypeWithoutGenerics.lastIndexOf('.') + 1); 79 | 80 | if (storage.hasTargetType(fqTargetType) && newElement.getText().equals(fqTargetType)) { 81 | processTargetTypeChangeEvent(newElement, fqTargetType, shortenedTargetType, document); 82 | updater.updateAllHighlighters(document, event.getNewChild().getTextRange().getEndOffset()); 83 | } 84 | } 85 | 86 | private void processSourceTypeChangeEvent(PsiElement oldElement, String sourceType, Document document) { 87 | final TextRange range = TextRange.create( 88 | oldElement.getTextRange().getStartOffset(), 89 | oldElement.getTextRange().getEndOffset() 90 | ); 91 | final RangeMarker rangeMarker = document.createRangeMarker(range); 92 | final var state = TypeChangeRefactoringProvider.getInstance(project).getState(); 93 | state.uncompletedTypeChanges.put(rangeMarker, sourceType); 94 | } 95 | 96 | private void processTargetTypeChangeEvent(PsiElement newElement, String targetType, String shortenedTargetType, Document document) { 97 | final var newRange = TextRange.from(newElement.getTextRange().getStartOffset(), shortenedTargetType.length()); 98 | final var state = TypeChangeRefactoringProvider.getInstance(project).getState(); 99 | 100 | RangeMarker relevantOldRangeMarker = null; 101 | String relevantSourceType = null; 102 | for (var entry : state.uncompletedTypeChanges.entrySet()) { 103 | final RangeMarker oldRangeMarker = entry.getKey(); 104 | final String sourceType = entry.getValue(); 105 | if (oldRangeMarker.getDocument() != document) continue; 106 | final var oldRange = new TextRange(oldRangeMarker.getStartOffset(), oldRangeMarker.getEndOffset()); 107 | if (oldRange.intersects(newRange) && storage.findPattern(sourceType, targetType).isPresent()) { 108 | relevantOldRangeMarker = oldRangeMarker; 109 | relevantSourceType = sourceType; 110 | break; 111 | } 112 | } 113 | if (relevantOldRangeMarker == null || relevantSourceType == null) return; 114 | state.uncompletedTypeChanges.remove(relevantOldRangeMarker); 115 | 116 | final var newRangeMarker = document.createRangeMarker(newRange); 117 | state.addCompletedTypeChange(relevantOldRangeMarker, newRangeMarker, relevantSourceType, targetType); 118 | state.refactoringEnabled = true; 119 | 120 | Thread disablerWaitThread = new Thread(() -> { 121 | try { 122 | Thread.sleep(TypeChangeSettingsState.getInstance().disableIntentionTimeout); 123 | state.refactoringEnabled = false; 124 | state.removeAllTypeChangesByRange(newRange); 125 | state.uncompletedTypeChanges.clear(); 126 | } catch (InterruptedException e) { 127 | LOG.error(e); 128 | } 129 | }); 130 | disablerWaitThread.start(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/legacy/TypeRelevantUsagesProcessor.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.legacy; 2 | 3 | import com.intellij.lang.java.JavaLanguage; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.psi.PsiElement; 6 | import com.intellij.psi.PsiExpression; 7 | import com.intellij.psi.PsiReference; 8 | import com.intellij.psi.PsiVariable; 9 | import com.intellij.psi.search.SearchScope; 10 | import com.intellij.psi.search.searches.ReferencesSearch; 11 | 12 | import java.util.*; 13 | 14 | import static org.jetbrains.research.ddtm.utils.PsiRelatedUtils.getContainingStatement; 15 | 16 | public class TypeRelevantUsagesProcessor { 17 | private final Project project; 18 | private LinkedList roots; 19 | private final SearchScope globalSearchScope; 20 | private Set allRelevantUsages; 21 | 22 | public TypeRelevantUsagesProcessor(Project project, PsiElement root, SearchScope globalSearchScope) { 23 | this.project = project; 24 | this.roots = new LinkedList<>(Collections.singletonList(root)); 25 | this.globalSearchScope = globalSearchScope; 26 | } 27 | 28 | public Set getAllRelevantUsages() { 29 | allRelevantUsages = new HashSet<>(); 30 | findRootsAndUsages(); 31 | return allRelevantUsages; 32 | } 33 | 34 | public void addMigrationRoot(PsiElement root) { 35 | roots.add(root); 36 | } 37 | 38 | private void saveUsage(PsiElement element) { 39 | allRelevantUsages.add(element); 40 | } 41 | 42 | private static PsiReference[] findLocalUsages(PsiElement element, SearchScope scope) { 43 | return ReferencesSearch 44 | .search(element, scope, false) 45 | .toArray(PsiReference.EMPTY_ARRAY); 46 | } 47 | 48 | private void findRootsAndUsages() { 49 | while (!roots.isEmpty()) { 50 | final List currentRoots = new ArrayList<>(roots); 51 | roots = new LinkedList<>(); 52 | for (final PsiElement root : currentRoots) { 53 | processRootElement(root); 54 | saveUsage(root); 55 | final Set processed = new HashSet<>(); 56 | final PsiReference[] rootUsages = findLocalUsages(root, globalSearchScope); 57 | for (PsiReference usage : rootUsages) { 58 | processRootUsageExpression(usage, processed); 59 | } 60 | } 61 | } 62 | } 63 | 64 | private void processRootElement(PsiElement root) { 65 | if (root instanceof PsiVariable || root instanceof PsiExpression) { 66 | final PsiElement element = getContainingStatement(root); 67 | element.accept(new TypeRelevantUsagesVisitor(this)); 68 | } 69 | } 70 | 71 | private void processRootUsageExpression(final PsiReference usage, final Set processed) { 72 | final PsiElement ref = usage.getElement(); 73 | if (ref.getLanguage() == JavaLanguage.INSTANCE) { 74 | saveUsage(ref); 75 | final PsiElement element = getContainingStatement(ref); 76 | if (element != null && !processed.contains(element)) { 77 | processed.add(element); 78 | ref.accept(new TypeRelevantUsagesVisitor(this)); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/legacy/TypeRelevantUsagesVisitor.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.legacy; 2 | 3 | import com.intellij.psi.*; 4 | import com.intellij.psi.util.PsiTreeUtil; 5 | 6 | public class TypeRelevantUsagesVisitor extends JavaRecursiveElementVisitor { 7 | private final TypeRelevantUsagesProcessor processor; 8 | 9 | TypeRelevantUsagesVisitor(TypeRelevantUsagesProcessor processor) { 10 | this.processor = processor; 11 | } 12 | 13 | @Override 14 | public void visitReturnStatement(PsiReturnStatement statement) { 15 | super.visitReturnStatement(statement); 16 | 17 | final PsiElement method = PsiTreeUtil.getParentOfType(statement, PsiMethod.class); 18 | final PsiExpression value = statement.getReturnValue(); 19 | 20 | if (method != null && value != null) { 21 | processor.addMigrationRoot(method); 22 | } 23 | } 24 | 25 | @Override 26 | public void visitLocalVariable(PsiLocalVariable variable) { 27 | super.visitLocalVariable(variable); 28 | } 29 | 30 | @Override 31 | public void visitLambdaExpression(PsiLambdaExpression expression) { 32 | } 33 | 34 | @Override 35 | public void visitClass(PsiClass aClass) { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/legacy/Utils.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.legacy; 2 | 3 | import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; 4 | import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl; 5 | import com.intellij.codeInsight.daemon.impl.DaemonProgressIndicator; 6 | import com.intellij.openapi.application.ApplicationManager; 7 | import com.intellij.openapi.diagnostic.Logger; 8 | import com.intellij.openapi.editor.Document; 9 | import com.intellij.openapi.progress.ProcessCanceledException; 10 | import com.intellij.openapi.progress.ProgressIndicator; 11 | import com.intellij.openapi.progress.ProgressManager; 12 | import com.intellij.openapi.progress.Task; 13 | import com.intellij.openapi.progress.util.AbstractProgressIndicatorExBase; 14 | import com.intellij.openapi.project.DumbService; 15 | import com.intellij.openapi.project.Project; 16 | import com.intellij.openapi.wm.ex.ProgressIndicatorEx; 17 | import com.intellij.psi.PsiFile; 18 | import org.jetbrains.annotations.NotNull; 19 | import org.jetbrains.research.ddtm.data.TypeChangeRulesStorage; 20 | 21 | public class Utils { 22 | private static final Logger LOG = Logger.getInstance(TypeChangeRulesStorage.class); 23 | 24 | public static void checkForCompilationErrors(PsiFile file, Document document, Project project) { 25 | 26 | ApplicationManager.getApplication().invokeLater(new Runnable() { 27 | @Override 28 | public void run() { 29 | ProgressManager.getInstance().run(new Task.Backgroundable(project, "Performing code analysis...") { 30 | @Override 31 | public void run(@NotNull ProgressIndicator indicator) { 32 | try { 33 | final ProgressIndicator daemonIndicator = new DaemonProgressIndicator(); 34 | ((ProgressIndicatorEx) indicator).addStateDelegate(new AbstractProgressIndicatorExBase() { 35 | @Override 36 | public void cancel() { 37 | super.cancel(); 38 | daemonIndicator.cancel(); 39 | } 40 | }); 41 | 42 | final DaemonCodeAnalyzerImpl codeAnalyzer = 43 | (DaemonCodeAnalyzerImpl) DaemonCodeAnalyzer.getInstance(project); 44 | final var infos = DumbService.getInstance(project).runReadActionInSmartMode(() -> 45 | codeAnalyzer.runMainPasses(file, document, daemonIndicator) 46 | ); 47 | LOG.debug(infos.toString()); 48 | 49 | } catch (ProcessCanceledException e) { 50 | LOG.info("Code analysis canceled", e); 51 | } catch (Exception e) { 52 | LOG.error(e); 53 | } 54 | } 55 | }); 56 | } 57 | }); 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/utils/EditorUtils.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.utils; 2 | 3 | import com.intellij.openapi.editor.RangeMarker; 4 | import com.intellij.openapi.util.TextRange; 5 | 6 | public class EditorUtils { 7 | public static boolean intersects(RangeMarker marker1, RangeMarker marker2) { 8 | final var range1 = new TextRange(marker1.getStartOffset(), marker1.getEndOffset()); 9 | final var range2 = new TextRange(marker2.getStartOffset(), marker2.getEndOffset()); 10 | return range1.intersects(range2); 11 | } 12 | 13 | public static boolean intersects(RangeMarker marker, TextRange range) { 14 | final var rangeFromMarker = new TextRange(marker.getStartOffset(), marker.getEndOffset()); 15 | return rangeFromMarker.intersects(range); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/utils/PsiRelatedUtils.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.utils; 2 | 3 | import com.intellij.ide.highlighter.JavaFileType; 4 | import com.intellij.openapi.diagnostic.Logger; 5 | import com.intellij.psi.*; 6 | import com.intellij.psi.util.PsiTreeUtil; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | public class PsiRelatedUtils { 11 | private static final Logger LOG = Logger.getInstance(PsiRelatedUtils.class); 12 | 13 | public static String[] splitByTokens(String source) { 14 | return source.split("[\\s.()<>]+"); 15 | } 16 | 17 | public static @Nullable T getHighestParentOfType( 18 | @Nullable PsiElement element, @NotNull Class aClass 19 | ) { 20 | PsiElement currentContext = element; 21 | while (true) { 22 | T parent = PsiTreeUtil.getParentOfType(currentContext, aClass); 23 | if (parent == null) { 24 | if (aClass.isInstance(currentContext)) { 25 | return aClass.cast(currentContext); 26 | } else { 27 | return null; 28 | } 29 | } 30 | currentContext = parent; 31 | } 32 | } 33 | 34 | @Nullable 35 | public static PsiType getTypeOfCodeFragment(PsiTypeCodeFragment fragment) { 36 | try { 37 | return fragment.getType(); 38 | } catch (PsiTypeCodeFragment.TypeSyntaxException | PsiTypeCodeFragment.NoTypeException e) { 39 | LOG.debug(e); 40 | return null; 41 | } 42 | } 43 | 44 | @Nullable 45 | public static PsiType getExpectedType(PsiElement element) { 46 | if (element instanceof PsiVariable) { 47 | return ((PsiVariable) element).getType(); 48 | } else if (element instanceof PsiAssignmentExpression) { 49 | PsiType type = ((PsiAssignmentExpression) element).getLExpression().getType(); 50 | return !PsiType.NULL.equals(type) ? type : null; 51 | } else if (element instanceof PsiMethod) { 52 | return ((PsiMethod) element).getReturnType(); 53 | } 54 | return null; 55 | } 56 | 57 | @Nullable 58 | public static PsiTypeElement getClosestPsiTypeElement(PsiElement element) { 59 | PsiTypeElement correspondingTypeElement = null; 60 | 61 | if (element instanceof PsiWhiteSpace) { 62 | if (element.getPrevSibling() instanceof PsiTypeElement) { 63 | correspondingTypeElement = (PsiTypeElement) element.getPrevSibling(); 64 | } else if (element.getNextSibling() instanceof PsiTypeElement) { 65 | correspondingTypeElement = (PsiTypeElement) element.getNextSibling(); 66 | } 67 | } else { 68 | correspondingTypeElement = PsiRelatedUtils.getHighestParentOfType(element, PsiTypeElement.class); 69 | } 70 | 71 | return correspondingTypeElement; 72 | } 73 | 74 | public static Boolean shouldIgnoreFile(PsiFile file) { 75 | return !file.isPhysical() || file instanceof PsiBinaryFile || file instanceof PsiCodeFragment 76 | || !(file.getFileType().equals(JavaFileType.INSTANCE)); 77 | } 78 | 79 | public static Boolean hasRootInside(PsiElement context, String currentRootName) { 80 | return context.getText().equals(currentRootName) || 81 | PsiTreeUtil.findChildrenOfType(context, PsiReferenceExpression.class).stream() 82 | .anyMatch(element -> element.getText().equals(currentRootName)); 83 | } 84 | 85 | public static PsiElement getContainingStatement(final PsiElement root) { 86 | final PsiStatement statement = PsiTreeUtil.getParentOfType(root, PsiStatement.class); 87 | PsiExpression condition = getContainingCondition(root, statement); 88 | if (condition != null) return condition; 89 | final PsiField field = PsiTreeUtil.getParentOfType(root, PsiField.class); 90 | return statement != null ? statement : field != null ? field : root; 91 | } 92 | 93 | public static PsiExpression getContainingCondition(PsiElement root, PsiStatement statement) { 94 | PsiExpression condition = null; 95 | if (statement instanceof PsiConditionalLoopStatement) { 96 | condition = ((PsiConditionalLoopStatement) statement).getCondition(); 97 | } else if (statement instanceof PsiIfStatement) { 98 | condition = ((PsiIfStatement) statement).getCondition(); 99 | } 100 | return PsiTreeUtil.isAncestor(condition, root, false) ? condition : null; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.utils; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public class StringUtils { 6 | public static String escapeSSRTemplates(String input) { 7 | String[] genericTypes = {"T", "U", "V", "S", "K"}; 8 | for (int i = 1; i <= 5; ++i) { 9 | input = input.replace(String.format("$%d$", i), genericTypes[i - 1]); 10 | } 11 | return input.replace("java.lang.", "") 12 | .replace(", ", ",") 13 | .replace(",", ", "); 14 | } 15 | 16 | // Linux or Windows absolute path 17 | private static final Pattern systemPathPattern = 18 | Pattern.compile("^/|(/[a-zA-Z0-9_.-]+)+$" + "|" + "^([a-zA-Z]:)?(\\\\[a-zA-Z0-9_.-]+)+\\\\?$"); 19 | 20 | public static boolean isSystemPath(String source) { 21 | return source != null && !source.trim().isEmpty() && systemPathPattern.matcher(source).matches(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/jetbrains/research/ddtm/utils/TypeReference.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.utils; 2 | 3 | import java.lang.reflect.Constructor; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.lang.reflect.ParameterizedType; 6 | import java.lang.reflect.Type; 7 | 8 | /** 9 | * References a generic type. 10 | * 11 | * @author crazybob@google.com (Bob Lee) 12 | */ 13 | public abstract class TypeReference { 14 | 15 | private final Type type; 16 | private volatile Constructor constructor; 17 | 18 | protected TypeReference() { 19 | Type superclass = getClass().getGenericSuperclass(); 20 | if (superclass instanceof Class) { 21 | throw new RuntimeException("Missing type parameter."); 22 | } 23 | this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; 24 | } 25 | 26 | /** 27 | * Instantiates a new instance of {@code T} using the default, no-arg 28 | * constructor. 29 | */ 30 | @SuppressWarnings("unchecked") 31 | public T newInstance() 32 | throws NoSuchMethodException, IllegalAccessException, 33 | InvocationTargetException, InstantiationException { 34 | if (constructor == null) { 35 | Class rawType = type instanceof Class 36 | ? (Class) type 37 | : (Class) ((ParameterizedType) type).getRawType(); 38 | constructor = rawType.getConstructor(); 39 | } 40 | return (T) constructor.newInstance(); 41 | } 42 | 43 | /** 44 | * Gets the referenced type. 45 | */ 46 | public Type getType() { 47 | return this.type; 48 | } 49 | } -------------------------------------------------------------------------------- /plugin/src/main/resources/DataDrivenTypeMigrationBundle.properties: -------------------------------------------------------------------------------- 1 | suggested.gutter.popup.header=Suggested type change: 2 | suggested.gutter.popup.button=Migrate 3 | suggested.gutter.popup.content=
from:
  {0}
to:
  {1}
4 | suggested.gutter.icon.tooltip.text=Suggested type migration refactoring 5 | suggested.gutter.popup.scope.combobox.label=Search scope: 6 | intention.list.item.text=Migrate to {0} 7 | intention.list.caption=Type Migration Rules 8 | intention.family.name=Data-driven type migration 9 | intention.suggested.text=Apply suggested type change 10 | settings.display.name=Data-Driven Type Migration 11 | settings.timeout.spinner.label=Timeout before disabling reactive type-change intention (ms): 12 | settings.scope.combobox.label=Search scope for type migration: 13 | settings.scope.local=Local scope 14 | settings.scope.file=Current file 15 | settings.scope.project=All project 16 | tool.window.content.name=Failed Type Changes 17 | tool.window.initial.position.text=No failed type changes found 18 | tool.window.code.usages.string=Found failed type changes 19 | command.name=Data-Driven Type Migration 20 | group.id=ddtm 21 | inspection.problem.descriptor=Found types that can be migrated: {0} 22 | inspection.simple.family.name=Migrate this type with DDTM plugin 23 | inspection.smart.string.to.path=Could be converted to 'java.nio.Path' 24 | notification.balloon.content=Usages migrated: {0}, usages failed: {1} 25 | dialog.migration.rules.combobox.label=Migrate {0} to: -------------------------------------------------------------------------------- /plugin/src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | org.jetbrains.research.java-types-migration 3 | Data-Driven Type Migration 4 | JetBrains Research 5 | 6 | 7 | 11 | 12 | 14 | com.intellij.modules.platform 15 | com.intellij.modules.java 16 | Git4Idea 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 30 | 31 | 32 | org.jetbrains.research.ddtm.ide.intentions.ProactiveTypeChangeIntention 33 | Type migration 34 | 35 | 36 | 38 | 39 | 41 | 42 | 43 | 45 | 46 | 53 | 54 | 55 | 56 | 58 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /plugin/src/main/resources/dbp-ddtm-count.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "dbp.ddtm.count", 3 | "title": "Data-Driven Type Migration plugin usage", 4 | "description": "Records the usages of the Data-Driven Type Migration plugin.", 5 | "recorder": "DBP", 6 | "type": "counter", 7 | "products": [ 8 | "IU", 9 | "IC" 10 | ], 11 | "baselines": { 12 | "primary": { 13 | "event": "registered" 14 | } 15 | }, 16 | "authors": [ 17 | { 18 | "name": "Oleg Smirnov" 19 | }, 20 | { 21 | "name": "Zarina Kurbatova" 22 | } 23 | ], 24 | "schema": { 25 | "tiger": [ 26 | { 27 | "event": "refactoring.intention.applied", 28 | "title": "Intention applied", 29 | "description": "The type change intention was applied.", 30 | "revisions": [ 31 | { 32 | "version": "1", 33 | "notes": [ 34 | "FUS-651", 35 | "FUS-652" 36 | ], 37 | "data": [ 38 | { 39 | "path": "type_change_id", 40 | "value": [ 41 | "{regexp#integer}" 42 | ], 43 | "column": { 44 | "index": 0, 45 | "title": "Type Change ID", 46 | "description": "The ID of the applied Type Change refactoring." 47 | } 48 | }, 49 | { 50 | "path": "migration_root", 51 | "value": [ 52 | "{util#class_name}" 53 | ], 54 | "column": { 55 | "index": 1, 56 | "title": "Migration root", 57 | "description": "The fully qualified classname of the PSI element corresponding to the migration root." 58 | } 59 | }, 60 | { 61 | "path": "unique_rules_used", 62 | "value": [ 63 | "{regexp#integer}" 64 | ], 65 | "column": { 66 | "index": 2, 67 | "title": "Unique rules used", 68 | "description": "The total number of unique rewrite rules which were used for type migration." 69 | } 70 | }, 71 | { 72 | "path": "usages_updated", 73 | "value": [ 74 | "{regexp#integer}" 75 | ], 76 | "column": { 77 | "index": 3, 78 | "title": "Usages updated", 79 | "description": "The total number of successfully updated usages after type migration." 80 | } 81 | }, 82 | { 83 | "path": "suspicious_usages_found", 84 | "value": [ 85 | "{regexp#integer}" 86 | ], 87 | "column": { 88 | "index": 4, 89 | "title": "Suspicious usages found", 90 | "description": "The total number of usages that could be potentially updated but shouldn't, since they are changing the return type of the corresponding expression." 91 | } 92 | }, 93 | { 94 | "path": "usages_failed", 95 | "value": [ 96 | "{regexp#integer}" 97 | ], 98 | "column": { 99 | "index": 5, 100 | "title": "Usages failed", 101 | "description": "The total number of failed-to-migrate usages." 102 | } 103 | }, 104 | { 105 | "path": "invocation_workflow", 106 | "value": [ 107 | "proactive", 108 | "reactive", 109 | "inspecting" 110 | ], 111 | "column": { 112 | "index": 6, 113 | "title": "Invocation workflow", 114 | "description": "The way of intention invocation: proactive (as general Code Intention), reactive (as Suggested Refactoring) or as inspection." 115 | } 116 | } 117 | ] 118 | } 119 | ] 120 | }, 121 | { 122 | "event": "recovering.intention.applied", 123 | "title": "Recovering intention applied", 124 | "description": "The recovering intention was applied.", 125 | "revisions": [ 126 | { 127 | "version": "1", 128 | "notes": [ 129 | "FUS-651", 130 | "FUS-652" 131 | ], 132 | "data": [ 133 | { 134 | "path": "type_change_id", 135 | "value": [ 136 | "{regexp#integer}" 137 | ], 138 | "column": { 139 | "index": 0, 140 | "title": "Type Change ID", 141 | "description": "The ID of the applied Type Change refactoring." 142 | } 143 | } 144 | ] 145 | } 146 | ] 147 | }, 148 | { 149 | "event": "gutter.icon.clicked", 150 | "title": "Gutter icon clicked", 151 | "description": "The migration was launched after the user clicked on the icon on the gutter.", 152 | "revisions": [ 153 | { 154 | "version": "1", 155 | "notes": [ 156 | "FUS-651", 157 | "FUS-652" 158 | ] 159 | } 160 | ] 161 | }, 162 | { 163 | "event": "rename.performed", 164 | "title": "Rename Refactoring performed", 165 | "description": "The built-in Rename Refactoring was performed.", 166 | "revisions": [ 167 | { 168 | "version": "1", 169 | "notes": [ 170 | "FUS-651", 171 | "FUS-652" 172 | ], 173 | "data": [ 174 | { 175 | "path": "element_canonical_name", 176 | "value": [ 177 | "{util#class_name}" 178 | ], 179 | "column": { 180 | "index": 0, 181 | "title": "Element canonical name", 182 | "description": "The fully qualified name of the class of the renamed PSI element." 183 | } 184 | } 185 | ] 186 | } 187 | ] 188 | }, 189 | { 190 | "event": "migration.undone", 191 | "title": "Type Migration undone", 192 | "description": "Indicates that previously applied data-driven type migration refactoring was undone by the developer.", 193 | "revisions": [ 194 | { 195 | "version": "1", 196 | "notes": [ 197 | "FUS-651", 198 | "FUS-652" 199 | ], 200 | "data": [ 201 | { 202 | "path": "type_change_id", 203 | "value": [ 204 | "{regexp#integer}" 205 | ], 206 | "column": { 207 | "index": 0, 208 | "title": "Type Change ID", 209 | "description": "The ID of the applied Type Change refactoring." 210 | } 211 | } 212 | ] 213 | } 214 | ] 215 | } 216 | ] 217 | } 218 | } -------------------------------------------------------------------------------- /plugin/src/main/resources/inspectionDescriptions/DDTM.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Highlights program elements (types) in the developer's Java code, which can be potentially migrated using the DDTM 4 | plugin. 5 |
6 | Generally, it has a WARNING level to attract the user's attention during the evaluation of the plugin, but it is mostly 7 | of a recommendation and training nature. 8 | 9 | -------------------------------------------------------------------------------- /plugin/src/main/resources/intentionDescriptions/ProactiveTypeChangeIntention/description.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Data-driven type migration. 5 |

6 | 7 |

8 | Data-driven type migration. 9 |

10 | 11 | -------------------------------------------------------------------------------- /plugin/src/test/java/org/jetbrains/research/ddtm/ide/TypeChangeIntentionTest.java: -------------------------------------------------------------------------------- 1 | package org.jetbrains.research.ddtm.ide; 2 | 3 | import com.intellij.openapi.command.WriteCommandAction; 4 | import com.intellij.openapi.projectRoots.JavaSdk; 5 | import com.intellij.openapi.projectRoots.Sdk; 6 | import com.intellij.openapi.vfs.VirtualFile; 7 | import com.intellij.pom.java.LanguageLevel; 8 | import com.intellij.psi.PsiElement; 9 | import com.intellij.psi.PsiFile; 10 | import com.intellij.psi.PsiManager; 11 | import com.intellij.testFramework.LightProjectDescriptor; 12 | import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; 13 | import org.jetbrains.annotations.NotNull; 14 | import org.jetbrains.research.ddtm.data.TypeChangeRulesStorage; 15 | import org.jetbrains.research.ddtm.data.enums.InvocationWorkflow; 16 | import org.jetbrains.research.ddtm.ide.migration.TypeChangeProcessor; 17 | import org.jetbrains.research.ddtm.utils.TypeReference; 18 | 19 | import java.io.File; 20 | import java.nio.file.Path; 21 | import java.util.List; 22 | import java.util.Set; 23 | 24 | public class TypeChangeIntentionTest extends LightJavaCodeInsightFixtureTestCase { 25 | private static final String JDK_HOME_DIRECTORY = System.getProperty("jdk.home.path"); 26 | private static final String MOCK_JDK_1_8_NAME = "mockJDK-1.8"; 27 | private static final String INITIAL_FILE_NAME = "Initial.java"; 28 | private static final String EXPECTED_FILE_NAME = "Expected.java"; 29 | 30 | @Override 31 | protected String getTestDataPath() { 32 | return Path.of("src", "test", "resources", "testData").toString(); 33 | } 34 | 35 | private String getMockJdkPath() { 36 | return Path.of("src", "test", "resources", MOCK_JDK_1_8_NAME).toString(); 37 | } 38 | 39 | @Override 40 | protected @NotNull LightProjectDescriptor getProjectDescriptor() { 41 | return new ProjectDescriptor(LanguageLevel.JDK_1_8) { 42 | @Override 43 | public Sdk getSdk() { 44 | return JavaSdk.getInstance().createJdk(Path.of(JDK_HOME_DIRECTORY).getFileName().toString(), JDK_HOME_DIRECTORY, false); 45 | } 46 | }; 47 | } 48 | 49 | protected void doTest(String testName, TypeReference sourceType, TypeReference targetType) { 50 | final VirtualFile directory = myFixture.copyDirectoryToProject(testName, testName); 51 | final VirtualFile initialFile = directory.findChild(INITIAL_FILE_NAME); 52 | final VirtualFile expectedFile = directory.findChild(EXPECTED_FILE_NAME); 53 | assertNotNull(initialFile); 54 | assertNotNull(expectedFile); 55 | 56 | myFixture.configureFromExistingVirtualFile(initialFile); 57 | final int offset = myFixture.getCaretOffset(); 58 | PsiElement element = myFixture.getFile().findElementAt(offset); 59 | final var storage = getProject().getService(TypeChangeRulesStorage.class); 60 | 61 | final var result = storage.findPattern(sourceType.getType().getTypeName(), targetType.getType().getTypeName()); 62 | assertTrue("No such pattern with specified types", result.isPresent()); 63 | final var pattern = result.get(); 64 | 65 | final var processor = (new TypeChangeProcessor(getProject(), InvocationWorkflow.PROACTIVE)); 66 | WriteCommandAction.runWriteCommandAction(getProject(), () -> processor.run(element, pattern)); 67 | 68 | PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(expectedFile); 69 | assertNotNull(psiFile); 70 | 71 | String expectedSrc = psiFile.getText().strip(); 72 | String actualSrc = getFile().getText().strip(); 73 | assertEquals(expectedSrc, actualSrc); 74 | } 75 | 76 | public void testFileToPath() { 77 | doTest("TestFileToPath", 78 | new TypeReference() { 79 | }, 80 | new TypeReference() { 81 | } 82 | ); 83 | } 84 | 85 | public void testListToSet() { 86 | doTest("TestListToSet", 87 | new TypeReference>() { 88 | }, 89 | new TypeReference>() { 90 | } 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /plugin/src/test/resources/mockJDK-1.8/jre/lib/annotations.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/data-driven-type-migration/9f0263be53dc34993f9aae9e8f056f1491efda87/plugin/src/test/resources/mockJDK-1.8/jre/lib/annotations.jar -------------------------------------------------------------------------------- /plugin/src/test/resources/mockJDK-1.8/jre/lib/rt.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JetBrains-Research/data-driven-type-migration/9f0263be53dc34993f9aae9e8f056f1491efda87/plugin/src/test/resources/mockJDK-1.8/jre/lib/rt.jar -------------------------------------------------------------------------------- /plugin/src/test/resources/testData/TestFileToPath/Expected.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import java.io.File; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | 8 | class FileToPathTest { 9 | public Path openFile() { 10 | Path file = Paths.get("file.txt"); 11 | File another_file = new File("files", "file.txt"); 12 | if (Files.exists(file)) { 13 | System.out.print(file.getFileName()); 14 | if (file.toFile() == null) { 15 | return null; 16 | } 17 | } 18 | boolean canWriteToFile = Files.isWritable(file); 19 | Path f1 = file; 20 | System.out.print(func(f1)); 21 | return f1; 22 | } 23 | 24 | public String func(Path f) { 25 | return f.toAbsolutePath().toString(); 26 | } 27 | } -------------------------------------------------------------------------------- /plugin/src/test/resources/testData/TestFileToPath/Initial.java: -------------------------------------------------------------------------------- 1 | package org.example; 2 | 3 | import java.io.File; 4 | 5 | class FileToPathTest { 6 | public File openFile() { 7 | File file = new File("file.txt"); 8 | File another_file = new File("files", "file.txt"); 9 | if (file.exists()) { 10 | System.out.print(file.getName()); 11 | if (file == null) { 12 | return null; 13 | } 14 | } 15 | boolean canWriteToFile = file.canWrite(); 16 | File f1 = file; 17 | System.out.print(func(f1)); 18 | return f1; 19 | } 20 | 21 | public String func(File f) { 22 | return f.getAbsolutePath(); 23 | } 24 | } -------------------------------------------------------------------------------- /plugin/src/test/resources/testData/TestListToSet/Expected.java: -------------------------------------------------------------------------------- 1 | import java.util.LinkedList; 2 | import java.util.List; 3 | import java.util.Set; 4 | import java.util.stream.Collectors; 5 | 6 | public class ListToSetTest { 7 | public Set getData() { 8 | List data = new LinkedList(); 9 | data.add(""); 10 | var tmp = data.get(0); 11 | return data.stream().filter(it -> it.contains("haha")).collect(Collectors.toSet()); 12 | } 13 | 14 | public void main() { 15 | Set a = getData(); 16 | System.out.print(a.iterator().next()); 17 | } 18 | } -------------------------------------------------------------------------------- /plugin/src/test/resources/testData/TestListToSet/Initial.java: -------------------------------------------------------------------------------- 1 | import java.util.LinkedList; 2 | import java.util.List; 3 | import java.util.stream.Collectors; 4 | 5 | public class ListToSetTest { 6 | public List getData() { 7 | List data = new LinkedList(); 8 | data.add(""); 9 | var tmp = data.get(0); 10 | return data.stream().filter(it -> it.contains("haha")).collect(Collectors.toList()); 11 | } 12 | 13 | public void main() { 14 | List a = getData(); 15 | System.out.print(a.get(0)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /scripts/add_ranks.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | 4 | from bs4 import BeautifulSoup 5 | 6 | from logging_json_prepare import PATH_TO_TYPE_CHANGE_PATTERNS 7 | 8 | PATH_TO_TYPE_CHANGE_PATTERNS_WITH_RANKS = '../plugin/src/main/resources/rules_with_ranks.json' 9 | 10 | 11 | @dataclass 12 | class PatternInfo: 13 | source_type: str 14 | target_type: str 15 | rank: int 16 | 17 | 18 | def main(): 19 | with open('data/TypeChangeSummary.html', 'r') as file: 20 | html = file.read() 21 | soup = BeautifulSoup(html, 'html.parser') 22 | data = [] 23 | for tr in soup.table.find_all('tr'): 24 | tds = tr.find_all('td') 25 | if not tds: 26 | continue 27 | data.append( 28 | PatternInfo( 29 | source_type=tds[0].text.replace(':[v0]', '$1$').replace(':[v1]', '$2$'), 30 | target_type=tds[1].text.replace(':[v0]', '$1$').replace(':[v1]', '$2$'), 31 | rank=int(tds[4].text) 32 | ) 33 | ) 34 | data.sort(key=lambda pattern: pattern.rank, reverse=True) 35 | 36 | with open(PATH_TO_TYPE_CHANGE_PATTERNS, "r") as file: 37 | patterns = json.load(file) 38 | for pattern in patterns: 39 | for info in data: 40 | if info.source_type == pattern['From'] and info.target_type == pattern['To']: 41 | pattern['Rank'] = info.rank 42 | 43 | with open(PATH_TO_TYPE_CHANGE_PATTERNS_WITH_RANKS, "w") as file: 44 | json.dump(patterns, file) 45 | 46 | 47 | if __name__ == '__main__': 48 | main() 49 | -------------------------------------------------------------------------------- /scripts/logging_json_prepare.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | PATH_TO_TYPE_CHANGE_PATTERNS = "../plugin/src/main/resources/rules.json" 4 | PATH_TO_TYPE_CHANGE_PATTERNS_WITH_IDS = "../plugin/src/main/resources/rules_with_ids.json" 5 | PATH_TO_UNIQUE_TYPES = "../plugin/src/main/resources/enum.json" 6 | 7 | 8 | def extract_unique_types(): 9 | with open(PATH_TO_TYPE_CHANGE_PATTERNS, "r") as file: 10 | data = json.load(file) 11 | unique_types = set() 12 | for pattern in data: 13 | unique_types.add(pattern['From']) 14 | unique_types.add(pattern['To']) 15 | result = {"type_patterns": list(unique_types)} 16 | with open(PATH_TO_UNIQUE_TYPES, "w") as file: 17 | json.dump(result, file) 18 | 19 | 20 | def add_ids(): 21 | with open(PATH_TO_TYPE_CHANGE_PATTERNS, "r") as file: 22 | data = json.load(file) 23 | id = 1 24 | for pattern in data: 25 | pattern['ID'] = id 26 | id += 1 27 | with open(PATH_TO_TYPE_CHANGE_PATTERNS_WITH_IDS, "w") as file: 28 | json.dump(data, file) 29 | 30 | 31 | def extract_types_for_readme_table(): 32 | with open(PATH_TO_TYPE_CHANGE_PATTERNS, "r") as file: 33 | data = json.load(file) 34 | for pattern in data: 35 | source_type, target_type = pattern["From"], pattern["To"] 36 | row = f'| `{source_type}` | `{target_type}` |' 37 | row = row.replace("$1$", ":[type]").replace("$2$", ":[type2]").replace("3$", ":[type3]") 38 | row = row.replace("java.lang.", "") 39 | print(row) 40 | 41 | 42 | if __name__ == '__main__': 43 | add_ids() 44 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "data-driven-type-migration" 2 | include("evaluation") 3 | include("plugin") 4 | --------------------------------------------------------------------------------