Skip to content

JBRes-7955: Implement transformation that changes file location within the project#38

Open
Vladislav0Art wants to merge 66 commits intomainfrom
vartiukhov/feature/file-moving-transformation
Open

JBRes-7955: Implement transformation that changes file location within the project#38
Vladislav0Art wants to merge 66 commits intomainfrom
vartiukhov/feature/file-moving-transformation

Conversation

@Vladislav0Art
Copy link
Collaborator

@Vladislav0Art Vladislav0Art commented Feb 2, 2026

Description

Introduce a project-level transformation that moves the given file into a directory either 1) suggested by AI or 2) provided in the config as a destination entry.

Closes: JBRes-7955

Changes

  1. Implement a Koog AI agent that suggests a new directory for the given file within the project (see SuggestNewDirectory.kt‎)
  2. Introduce a facade for future suggestion APIs (see Suggestions.kt‎)
  3. Implement helper methods that extract properties from the transformation config in Config.kt‎.
  4. Implemented a single transformation (MoveFileToAiSuggestedDirectoryTransformation.kt‎) that has two directory selection strategies:
    1. When the directory is generated by AI (this transformation version is registered with ID of move-file-into-suggested-directory-transformation/ai)
    2. When the directory is given in the config as destination entry (this transformation version is registered with ID of move-file-into-suggested-directory-transformation/config)

The transformation uses MoveFilesOrDirectoriesProcessor.java to move a file.

@Vladislav0Art Vladislav0Art changed the title Implement transformation that changes file location in the project JBRes-7955: Implement transformation that changes file location within the project Feb 2, 2026
@github-actions
Copy link

github-actions bot commented Feb 3, 2026

Qodana Community for JVM

2 new problems were found

Inspection name Severity Problems
Unused symbol 🔶 Warning 1
Class member can have 'private' visibility ◽️ Notice 1

💡 Qodana analysis was run in the pull request mode: only the changed files were checked

View the detailed Qodana report

To be able to view the detailed Qodana report, you can either:

To get *.log files or any other Qodana artifacts, run the action with upload-result option set to true,
so that the action will upload the files as the job artifacts:

      - name: 'Qodana Scan'
        uses: JetBrains/qodana-action@v2025.1.1
        with:
          upload-result: true
Contact Qodana team

Contact us at qodana-support@jetbrains.com

@Vladislav0Art Vladislav0Art self-assigned this Feb 4, 2026
Copy link
Collaborator

@dragoi75 dragoi75 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a few nitpicks and some things that seem broken from my limited testing.
I could not test the AI-suggested one, see my comment in the file.

Also, why do you not use the refactoring provided by the IntelliJ SDK, see MoveFilesOrDirectoriesProcessor?
I am aware your implementation probably is similar, but I think there might be edge cases that we are not thinking about, so the SDK is probably a safer option.

As a general remark, this PR is too big, which makes reviewing difficult. In the future please consider splitting in smaller PRs.

@Vladislav0Art
Copy link
Collaborator Author

Vladislav0Art commented Feb 11, 2026

TODO(@Vladislav0Art): Use MoveFilesOrDirectoriesProcessor.java to move a file into a different directory (see this comment for details)

See `Map<String, Any>.require` and its tests in `ConfigParsingTest`.
…o-be-moved file

Next steps:
1. Update imports of to-be-moved file components to match a new package.
2. Move the file.
@Vladislav0Art Vladislav0Art force-pushed the vartiukhov/feature/file-moving-transformation branch from 1dedfc0 to 395568b Compare February 26, 2026 17:17
@Vladislav0Art Vladislav0Art marked this pull request as ready for review February 26, 2026 18:50
Copy link
Collaborator

@dragoi75 dragoi75 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall nice :D

The comments are mostly nits, but there are two that need addressing in MoveFileIntoSuggestedDirectoryTransformation.kt. One about visibility of classes in the moved files and one about error handling.

I try to do some more manual testing.

fileToMove: PsiFile,
suggestions: List<String>
): TransformationResult {
val filename = withReadAction { fileToMove.name }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the file you are moving contains non-public classes it doesn't seem to work.
I got the following error:

✗ Failed to move file OwnerController.java java.lang.RuntimeException: `com.intellij.refactoring.BaseRefactoringProcessor$ConflictsInTestsException`: Package-local class `OwnerController` will no longer be accessible from class `org.springframework.samples.petclinic.owner.OwnerControllerTests`

Claude suggested adding a method that makes the classes public. Not sure if changing them to public has other consequences, but see what it suggested below (seems to work in my testing):

/**
 * Ensures all top-level classes in the Java file are public.
 * This is necessary to avoid accessibility conflicts when moving files to different packages.
 */
private fun ensureClassesArePublic(project: Project, javaFile: PsiJavaFile) {
    val classesToModify = withReadAction {
        javaFile.classes.filter { psiClass ->
            val modifierList = psiClass.modifierList
            modifierList != null && !(PsiUtil.getAccessLevel(modifierList).equals(PsiUtil.ACCESS_LEVEL_PUBLIC))
        }
    }

    if (classesToModify.isNotEmpty()) {
        WriteCommandAction.runWriteCommandAction(project) {
            classesToModify.forEach { psiClass ->
                psiClass.modifierList?.setModifierProperty(PsiModifier.PUBLIC, true)
            }
        }
        // Commit changes after making classes public
        ApplicationManager.getApplication().invokeAndWait {
            PsiDocumentManager.getInstance(project).commitAllDocuments()
        }
    }
}

And you should call it before for (suggestion in suggestions) {.

Copy link
Collaborator Author

@Vladislav0Art Vladislav0Art Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the problem is not in the package-local classes but when these classes are in use in another file from the same package.

Simply converting them to public class won't help because, by Java rules, only one class in a file can be public; all the rest may only be package-local.


For now, I check whether any package-local file is in use inside other files; if so, we skip this transformation: b63876e

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point with the fact that only one class can be public, and yes, this does solve the issue, but limits a bit the scope of the transformation. I guess a tradeoff we have to accept.

@Vladislav0Art Vladislav0Art requested a review from dragoi75 March 7, 2026 23:35
Copy link
Collaborator

@dragoi75 dragoi75 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just 3 more nitpicks 🙃


logger.info(" ↳ Successfully moved $filename into $suggestion!")
return TransformationResult.Success(
message = "Successfully moved $filename into $suggestion. Usage Summary:\n$usageSummary",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space :) Meant to have it indented to be easier to skip when reading the logs.

package package1;

public class Component2 {
// This class MUST by in the package `package2`!
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: be instead of by.

fileToMove: PsiFile,
suggestions: List<String>
): TransformationResult {
val filename = withReadAction { fileToMove.name }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point with the fact that only one class can be public, and yes, this does solve the issue, but limits a bit the scope of the transformation. I guess a tradeoff we have to accept.

if (packageLocalClassesInUseByOtherFiles(fileToMove)) {
return TransformationResult.Skipped("Cannot move $filename: Package-local classes are in use by other files")
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider also adding a filter to not move test files. Otherwise it throws an error. One way to do it can be:

        val fileIndex = ProjectFileIndex.getInstance(project)
        if (withReadAction {fileIndex.isInTestSourceContent(fileToMove.virtualFile)}) {
            return TransformationResult.Skipped("Cannot move $filename: It is a test file")
        }

@Vladislav0Art
Copy link
Collaborator Author

Looks good, just 3 more nitpicks 🙃

Hi! Thanks! I'll address it shortly tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants