Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce additional File and Path Refaster rules #1282

Merged
merged 2 commits into from
Aug 22, 2024

Conversation

Stephan202
Copy link
Member

@Stephan202 Stephan202 commented Aug 11, 2024

Suggested commit message:

Introduce additional `File` and `Path` Refaster rules (#1282)

@Stephan202 Stephan202 added this to the 0.19.0 milestone Aug 11, 2024
Copy link

Looks good. All 4 mutations in this change were killed.

class surviving killed
🎉tech.picnic.errorprone.documentation.DocumentationGeneratorTaskListener 0 4

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member Author

@Stephan202 Stephan202 left a comment

Choose a reason for hiding this comment

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

Added some context. I expect that SonarCloud will flag the new createTempFile "calls"; if so, I'll push another commit with suppressions.

Comment on lines +24 to +51
/** Prefer the more idiomatic {@link Path#of(URI)} over {@link Paths#get(URI)}. */
static final class PathOfUri {
@BeforeTemplate
Path before(URI uri) {
return Paths.get(uri);
}

@AfterTemplate
Path after(URI uri) {
return Path.of(uri);
}
}

/**
* Prefer the more idiomatic {@link Path#of(String, String...)} over {@link Paths#get(String,
* String...)}.
*/
static final class PathOfString {
@BeforeTemplate
Path before(String first, @Repeated String more) {
return Paths.get(first, more);
}

@AfterTemplate
Path after(String first, @Repeated String more) {
return Path.of(first, more);
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

I recently added these two to my TODO list because I saw openrewrite/rewrite-migrate-java#519, but just now I noticed there was already an older identical TODO entry 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

One of the rare cases where our solutions are equal in length. ;)

Comment on lines +53 to +65
/** Avoid redundant conversions from {@link Path} to {@link File}. */
// XXX: Review whether a rule such as this one is better handled by the `IdentityConversion` rule.
static final class PathInstance {
@BeforeTemplate
Path before(Path path) {
return path.toFile().toPath();
}

@AfterTemplate
Path after(Path path) {
return path;
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

I added this because I realized that the two FilesCreateTempFile*ToFile rules below make it much more likely that a .toFile().toPath() chain is introduced.

Comment on lines 117 to 131
/**
* Prefer {@link Files#createTempFile(Path,String, String, FileAttribute[])} over alternatives
* that create files with more liberal permissions.
*/
static final class FilesCreateTempFileInCustomDirectoryToFile {
@BeforeTemplate
File before(File directory, String prefix, String suffix) throws IOException {
return File.createTempFile(prefix, suffix, directory);
}

@AfterTemplate
File after(File directory, String prefix, String suffix) throws IOException {
return Files.createTempFile(directory.toPath(), prefix, suffix).toFile();
}
}
Copy link
Member Author

Choose a reason for hiding this comment

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

As suggested by @timtebeek here.

@Stephan202
Copy link
Member Author

I expect that SonarCloud will flag the new createTempFile "calls"; if so, I'll push another commit with suppressions.

Didn't happen 👀. Likely due to the custom directory being specified.

static final class FilesCreateTempFileInCustomDirectoryToFile {
@BeforeTemplate
File before(File directory, String prefix, String suffix) throws IOException {
return File.createTempFile(prefix, suffix, directory);
Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, @timtebeek one thing to consider: File.createTempFile(prefix, suffix, null) is now matched by both rules. Not sure how OpenRewrite handles that? (Our custom Refaster integration resolves this kind of ambiguity by selecting the rule that produces the smallest replacement string, which in this case means that it (correctly) selects the rule above.)

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting! I suspect the generated recipe will contain a MethodPattern matching exactly one of these, not either one.

Copy link
Member Author

@Stephan202 Stephan202 Aug 11, 2024

Choose a reason for hiding this comment

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

I suppose that if OpenRewrite applies them in the listed order, the first one will win. Anyway, generated code attached 😄

Generated recipes
/**
 * OpenRewrite recipe created for Refaster template {@code FileRules.FilesCreateTempFileToFile}.
 */
@SuppressWarnings("all")
@NonNullApi
@Generated("org.openrewrite.java.template.processor.RefasterTemplateProcessor")
public static class FilesCreateTempFileToFileRecipe extends Recipe {

    /**
     * Instantiates a new instance.
     */
    public FilesCreateTempFileToFileRecipe() {}

    @Override
    public String getDisplayName() {
        return "Prefer `Files#createTempFile(String, String, FileAttribute[])` over alternatives that create files with more liberal permissions";
    }

    @Override
    public String getDescription() {
        return "Recipe created for the following Refaster template:\n```java\nstatic final class FilesCreateTempFileToFile {\n    \n    @BeforeTemplate\n    @SuppressWarnings(value = {\"FilesCreateTempFileInCustomDirectoryToFile\", \"java:S5443\", \"key-to-resolve-AnnotationUseStyle-and-TrailingComment-check-conflict\"})\n    File before(String prefix, String suffix) throws IOException {\n        return Refaster.anyOf(File.createTempFile(prefix, suffix), File.createTempFile(prefix, suffix, null));\n    }\n    \n    @AfterTemplate\n    @SuppressWarnings(value = \"java:S5443\")\n    File after(String prefix, String suffix) throws IOException {\n        return Files.createTempFile(prefix, suffix).toFile();\n    }\n}\n```\n.";
    }

    @Override
    public TreeVisitor<?, ExecutionContext> getVisitor() {
        JavaVisitor<ExecutionContext> javaVisitor = new AbstractRefasterJavaVisitor() {
            final JavaTemplate before$0 = JavaTemplate
                    .builder("java.io.File.createTempFile(#{prefix:any(java.lang.String)}, #{suffix:any(java.lang.String)})")
                    .build();
            final JavaTemplate before$1 = JavaTemplate
                    .builder("java.io.File.createTempFile(#{prefix:any(java.lang.String)}, #{suffix:any(java.lang.String)}, null)")
                    .build();
            final JavaTemplate after = JavaTemplate
                    .builder("java.nio.file.Files.createTempFile(#{prefix:any(java.lang.String)}, #{suffix:any(java.lang.String)}).toFile()")
                    .build();

            @Override
            public J visitMethodInvocation(J.MethodInvocation elem, ExecutionContext ctx) {
                JavaTemplate.Matcher matcher;
                if ((matcher = before$0.matcher(getCursor())).find()) {
                    return embed(
                            after.apply(getCursor(), elem.getCoordinates().replace(), matcher.parameter(0), matcher.parameter(1)),
                            getCursor(),
                            ctx,
                            SHORTEN_NAMES
                    );
                }
                if ((matcher = before$1.matcher(getCursor())).find()) {
                    return embed(
                            after.apply(getCursor(), elem.getCoordinates().replace(), matcher.parameter(0), matcher.parameter(1)),
                            getCursor(),
                            ctx,
                            SHORTEN_NAMES
                    );
                }
                return super.visitMethodInvocation(elem, ctx);
            }

        };
        return Preconditions.check(
                Preconditions.and(
                    new UsesType<>("java.io.File", true),
                    new UsesMethod<>("java.io.File createTempFile(..)")
                ),
                javaVisitor
        );
    }
}

/**
 * OpenRewrite recipe created for Refaster template {@code FileRules.FilesCreateTempFileInCustomDirectoryToFile}.
 */
@SuppressWarnings("all")
@NonNullApi
@Generated("org.openrewrite.java.template.processor.RefasterTemplateProcessor")
public static class FilesCreateTempFileInCustomDirectoryToFileRecipe extends Recipe {

    /**
     * Instantiates a new instance.
     */
    public FilesCreateTempFileInCustomDirectoryToFileRecipe() {}

    @Override
    public String getDisplayName() {
        return "Prefer `Files#createTempFile(Path,String, String, FileAttribute[])` over alternatives that create files with more liberal permissions";
    }

    @Override
    public String getDescription() {
        return "Recipe created for the following Refaster template:\n```java\nstatic final class FilesCreateTempFileInCustomDirectoryToFile {\n    \n    @BeforeTemplate\n    File before(File directory, String prefix, String suffix) throws IOException {\n        return File.createTempFile(prefix, suffix, directory);\n    }\n    \n    @AfterTemplate\n    File after(File directory, String prefix, String suffix) throws IOException {\n        return Files.createTempFile(directory.toPath(), prefix, suffix).toFile();\n    }\n}\n```\n.";
    }

    @Override
    public TreeVisitor<?, ExecutionContext> getVisitor() {
        JavaVisitor<ExecutionContext> javaVisitor = new AbstractRefasterJavaVisitor() {
            final JavaTemplate before = JavaTemplate
                    .builder("java.io.File.createTempFile(#{prefix:any(java.lang.String)}, #{suffix:any(java.lang.String)}, #{directory:any(java.io.File)})")
                    .build();
            final JavaTemplate after = JavaTemplate
                    .builder("java.nio.file.Files.createTempFile(#{directory:any(java.io.File)}.toPath(), #{prefix:any(java.lang.String)}, #{suffix:any(java.lang.String)}).toFile()")
                    .build();

            @Override
            public J visitMethodInvocation(J.MethodInvocation elem, ExecutionContext ctx) {
                JavaTemplate.Matcher matcher;
                if ((matcher = before.matcher(getCursor())).find()) {
                    return embed(
                            after.apply(getCursor(), elem.getCoordinates().replace(), matcher.parameter(2), matcher.parameter(0), matcher.parameter(1)),
                            getCursor(),
                            ctx,
                            SHORTEN_NAMES
                    );
                }
                return super.visitMethodInvocation(elem, ctx);
            }

        };
        return Preconditions.check(
                Preconditions.and(
                    new UsesType<>("java.io.File", true),
                    new UsesMethod<>("java.io.File createTempFile(..)")
                ),
                javaVisitor
        );
    }
}

Copy link
Contributor

@timtebeek timtebeek Aug 11, 2024

Choose a reason for hiding this comment

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

Figured a test would be quickest way to check, but my IDE is throwing up some error

java: exporting a package from system module jdk.compiler is not allowed with --release

tech.picnic.errorprone.refasterrules.FileRulesRecipesTest
package tech.picnic.errorprone.refasterrules;

import org.junit.jupiter.api.Test;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.java.Assertions.java;

final class FileRulesRecipesTest implements RewriteTest {
    @Override
    public void defaults(RecipeSpec spec) {
        spec.recipe(new FileRulesRecipes());
    }

    @Test
    void stringValueOf() {
        rewriteRun(
            //language=java
            java(
                """
                    import java.io.File;
                    import java.io.IOException;

                    class Test {
                        File twoArgs() throws IOException {
                            return File.createTempFile("foo", "bar");
                        }
                        File threeArgs() throws IOException {
                            return File.createTempFile("foo", "bar", new File("baz"));
                        }
                    }
                    """,
                """
                    import java.io.File;
                    import java.io.IOException;
                    import java.nio.file.Files;

                    class Test {
                        File twoArgs() throws IOException {
                            return Files.createTempFile("foo", "bar").toFile();
                        }
                        File threeArgs() throws IOException {
                            return Files.createTempFile(new File("baz").toPath(), "foo", "bar").toFile();
                        }
                    }
                    """
            )
        );
    }
}

Now that you're using Java 17 you could also look at this TODO again.

Copy link
Member

Choose a reason for hiding this comment

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

@timtebeek to fix that error; do the following:

If this happens, go to Settings -> Build, Execution, Deployment -> Compiler -> Java Compiler and deselect the option Use '--release' option for cross-compilation (Java 9 and later). See IDEA-288052 for details.

It's listed in the README :).

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks! With that the above test now passes. 🎉

Copy link
Member Author

Choose a reason for hiding this comment

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

I was worried about the File.createTempFile("foo", "bar", null), but when I modify the test like that it still passes 👍

Copy link
Contributor

@mohamedsamehsalah mohamedsamehsalah left a comment

Choose a reason for hiding this comment

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

🗄️

static final class PathInstance {
@BeforeTemplate
Path before(Path path) {
return path.toFile().toPath();
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add a similar rule for: File > Path > File?

Copy link
Member Author

Choose a reason for hiding this comment

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

I considered that, but IIUC none of the current rules would introduce such a chain. Also didn't find any occurrence internally. But if you prefer we can definitely add such a rule. You up for it? :)

@@ -63,4 +113,20 @@ File after(String prefix, String suffix) throws IOException {
return Files.createTempFile(prefix, suffix).toFile();
}
}

/**
* Prefer {@link Files#createTempFile(Path,String, String, FileAttribute[])} over alternatives
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Prefer {@link Files#createTempFile(Path,String, String, FileAttribute[])} over alternatives
* Prefer {@link Files#createTempFile(Path, String, String, FileAttribute[])} over alternatives

@rickie rickie force-pushed the sschroevers/more-path-rules branch from 681ca67 to 571113f Compare August 22, 2024 06:34
Copy link

Looks good. All 4 mutations in this change were killed.

class surviving killed
🎉tech.picnic.errorprone.documentation.DocumentationGeneratorTaskListener 0 4

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

1 similar comment
Copy link

Looks good. All 4 mutations in this change were killed.

class surviving killed
🎉tech.picnic.errorprone.documentation.DocumentationGeneratorTaskListener 0 4

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link

@rickie
Copy link
Member

rickie commented Aug 22, 2024

Anything else from you side @timtebeek 😄 ?

Otherwise I'll merge it :).

Copy link
Contributor

@timtebeek timtebeek 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 to me! Thanks for adding these patterns checks here. :)

@rickie rickie merged commit 4cbfdba into master Aug 22, 2024
16 checks passed
@rickie rickie deleted the sschroevers/more-path-rules branch August 22, 2024 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

4 participants