diff --git a/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java b/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java new file mode 100644 index 0000000000..4f6dc9d49e --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java @@ -0,0 +1,483 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.migrate.io; + +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.search.DeclaresType; +import org.openrewrite.java.tree.*; +import org.openrewrite.marker.SearchResult; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static java.util.Collections.emptyList; + +public class AddInputStreamBulkReadMethod extends Recipe { + + private static final String MARKER_MESSAGE = "Missing bulk read method may cause significant performance degradation"; + private static final String JAVA_IO_INPUT_STREAM = "java.io.InputStream"; + + @Override + public String getDisplayName() { + return "Add bulk read method to `InputStream` implementations"; + } + + @Override + public String getDescription() { + return "Adds a `read(byte[], int, int)` method to `InputStream` subclasses that only override the single-byte " + + "`read()` method. Java's default `InputStream.read(byte[], int, int)` implementation calls the " + + "single-byte `read()` method in a loop, which can cause severe performance degradation (up to 350x " + + "slower) for bulk reads. This recipe detects `InputStream` implementations that delegate to another " + + "stream and adds the missing bulk read method to delegate bulk reads as well."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new DeclaresType<>(JAVA_IO_INPUT_STREAM, true), new JavaIsoVisitor() { + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + + // Skip if not extending InputStream + if (cd.getExtends() == null || !TypeUtils.isAssignableTo(JAVA_IO_INPUT_STREAM, cd.getType())) { + return cd; + } + + // Skip FilterInputStream subclasses (they already delegate bulk reads) + if (TypeUtils.isAssignableTo("java.io.FilterInputStream", cd.getType())) { + return cd; + } + + return processInputStreamClass(cd, cd.getBody()); + } + + @Override + public J.NewClass visitNewClass(J.NewClass newClass, ExecutionContext ctx) { + J.NewClass nc = super.visitNewClass(newClass, ctx); + + // Must be an anonymous class + if (nc.getBody() == null) { + return nc; + } + + // Must extend InputStream + if (!TypeUtils.isAssignableTo(JAVA_IO_INPUT_STREAM, nc.getType())) { + return nc; + } + + // Skip FilterInputStream subclasses + if (TypeUtils.isAssignableTo("java.io.FilterInputStream", nc.getType())) { + return nc; + } + + return processInputStreamClass(nc, nc.getBody()); + } + + @SuppressWarnings("unchecked") + private T processInputStreamClass(T tree, J.Block body) { + AnalysisResult result = analyzeClass(body.getStatements()); + if (result == null || result.isHasBulkRead()) { + return tree; + } + + // No delegate found or complex body - add marker for manual implementation + if (result.getDelegate() == null || result.isComplex()) { + return SearchResult.found(tree, MARKER_MESSAGE); + } + + // Simple delegation - add bulk read method right after the single-byte read method + Statement bulkMethod = createBulkReadMethod(result.getDelegate(), result.isHasNullCheck(), result.isUsesIfStyle(), body); + J.Block newBody = body.withStatements(ListUtils.flatMap(body.getStatements(), + stmt -> stmt == result.getReadMethod() ? Arrays.asList(stmt, bulkMethod) : stmt)); + + if (tree instanceof J.ClassDeclaration) { + return (T) ((J.ClassDeclaration) tree).withBody(newBody); + } + if (tree instanceof J.NewClass) { + return (T) ((J.NewClass) tree).withBody(newBody); + } + return tree; + } + + private @Nullable AnalysisResult analyzeClass(List statements) { + J.MethodDeclaration readMethod = null; + boolean hasBulkRead = false; + + for (Statement stmt : statements) { + if (!(stmt instanceof J.MethodDeclaration)) { + continue; + } + J.MethodDeclaration method = (J.MethodDeclaration) stmt; + if (!"read".equals(method.getSimpleName())) { + continue; + } + if (method.getParameters().isEmpty() || + (method.getParameters().size() == 1 && method.getParameters().get(0) instanceof J.Empty)) { + readMethod = method; + } else if (method.getParameters().size() == 3) { + hasBulkRead = true; + } + } + + if (readMethod == null) { + return null; + } + + // Check if body is complex + boolean isComplex = isComplexBody(readMethod); + + // Find delegate - use simple finder for simple bodies, broader search for complex + String delegate; + if (isComplex) { + delegate = findAnyDelegate(readMethod); + } else { + delegate = findDelegate(readMethod); + } + String nullCheckVar = findNullCheckVariable(readMethod); + boolean hasNullCheck = nullCheckVar != null && nullCheckVar.equals(delegate); + + // Detect if null check uses if-statement style vs ternary style + List readMethodStatements = readMethod.getBody() != null ? + readMethod.getBody().getStatements() : emptyList(); + boolean usesIfStyle = readMethodStatements.size() == 2 && readMethodStatements.get(0) instanceof J.If; + + return new AnalysisResult(readMethod, hasBulkRead, delegate, hasNullCheck, usesIfStyle, isComplex); + } + + private boolean isComplexBody(J.MethodDeclaration method) { + if (method.getBody() == null) { + return true; + } + + List statements = method.getBody().getStatements(); + + // Single statement: return ... + if (statements.size() == 1) { + return !isSimpleReturnStatement(statements.get(0)); + } + + // Two statements: if (x == null) return -1; return x.read(); + if (statements.size() == 2) { + return !isNullCheckIfPattern(statements.get(0), statements.get(1)); + } + + // More than two statements is complex + return true; + } + + private boolean isSimpleReturnStatement(Statement stmt) { + if (!(stmt instanceof J.Return)) { + return false; + } + + J.Return ret = (J.Return) stmt; + Expression expr = ret.getExpression(); + if (expr == null) { + return false; + } + + // Handle ternary: delegate == null ? -1 : delegate.read() + if (expr instanceof J.Ternary) { + J.Ternary ternary = (J.Ternary) expr; + // Check condition is simple null check + if (!isSimpleNullCheck(ternary.getCondition())) { + return false; + } + // Check true part is a literal (like -1) + if (!(ternary.getTruePart() instanceof J.Literal) && + !(ternary.getTruePart() instanceof J.Unary)) { + return false; + } + expr = ternary.getFalsePart(); + } + + return isSimpleReadInvocation(expr); + } + + private boolean isNullCheckIfPattern(Statement first, Statement second) { + // First statement should be: if (x == null) return -1; + if (!(first instanceof J.If)) { + return false; + } + J.If ifStmt = (J.If) first; + + // Check condition is simple null check + if (!isSimpleNullCheck(ifStmt.getIfCondition().getTree())) { + return false; + } + + // Check then branch is a simple return with literal + Statement thenStmt = ifStmt.getThenPart(); + if (thenStmt instanceof J.Block) { + List thenStatements = ((J.Block) thenStmt).getStatements(); + if (thenStatements.size() != 1) { + return false; + } + thenStmt = thenStatements.get(0); + } + if (!(thenStmt instanceof J.Return)) { + return false; + } + J.Return thenReturn = (J.Return) thenStmt; + if (!(thenReturn.getExpression() instanceof J.Literal) && + !(thenReturn.getExpression() instanceof J.Unary)) { + return false; + } + + // Should not have else branch + if (ifStmt.getElsePart() != null) { + return false; + } + + // Second statement should be: return x.read(); + if (!(second instanceof J.Return)) { + return false; + } + J.Return ret = (J.Return) second; + return isSimpleReadInvocation(ret.getExpression()); + } + + private boolean isSimpleReadInvocation(@Nullable Expression expr) { + if (!(expr instanceof J.MethodInvocation)) { + return false; + } + + J.MethodInvocation mi = (J.MethodInvocation) expr; + if (!"read".equals(mi.getSimpleName())) { + return false; + } + + // Should have no arguments (single-byte read) + if (!mi.getArguments().isEmpty() && + !(mi.getArguments().size() == 1 && mi.getArguments().get(0) instanceof J.Empty)) { + return false; + } + + return true; + } + + private boolean isSimpleNullCheck(Expression condition) { + if (!(condition instanceof J.Binary)) { + return false; + } + J.Binary binary = (J.Binary) condition; + if (binary.getOperator() != J.Binary.Type.Equal) { + return false; + } + Expression left = binary.getLeft(); + Expression right = binary.getRight(); + if (J.Literal.isLiteralValue(left, null)) { + return right instanceof J.Identifier; + } + return left instanceof J.Identifier && J.Literal.isLiteralValue(right, null); + } + + private @Nullable String findDelegate(J.MethodDeclaration method) { + if (method.getBody() == null) { + return null; + } + + for (Statement stmt : method.getBody().getStatements()) { + if (!(stmt instanceof J.Return)) { + continue; + } + J.Return ret = (J.Return) stmt; + Expression expr = ret.getExpression(); + + // Handle ternary: delegate == null ? -1 : delegate.read() + if (expr instanceof J.Ternary) { + expr = ((J.Ternary) expr).getFalsePart(); + } + + // Look for method invocation of read() + if (!(expr instanceof J.MethodInvocation)) { + continue; + } + J.MethodInvocation mi = (J.MethodInvocation) expr; + if (!"read".equals(mi.getSimpleName()) || mi.getSelect() == null) { + continue; + } + + // The select must be an InputStream + if (!TypeUtils.isAssignableTo(JAVA_IO_INPUT_STREAM, mi.getSelect().getType())) { + continue; + } + + // Get the delegate name + if (mi.getSelect() instanceof J.Identifier) { + return ((J.Identifier) mi.getSelect()).getSimpleName(); + } + if (mi.getSelect() instanceof J.FieldAccess) { + J.FieldAccess fa = (J.FieldAccess) mi.getSelect(); + return fa.print(getCursor()); + } + } + return null; + } + + /** + * Broader search for any delegate.read() call in the method body. + * Used for complex bodies to determine if a marker should be added. + * Returns null if multiple different delegates are found (intentional design). + */ + private @Nullable String findAnyDelegate(J.MethodDeclaration method) { + if (method.getBody() == null) { + return null; + } + + Set foundDelegates = new JavaIsoVisitor>() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, Set foundDelegates) { + if ("read".equals(mi.getSimpleName()) && + mi.getSelect() != null && + TypeUtils.isAssignableTo(JAVA_IO_INPUT_STREAM, mi.getSelect().getType())) { + if (mi.getSelect() instanceof J.Identifier) { + foundDelegates.add(((J.Identifier) mi.getSelect()).getSimpleName()); + } else if (mi.getSelect() instanceof J.FieldAccess) { + foundDelegates.add(((J.FieldAccess) mi.getSelect()).getSimpleName()); + } + } + return super.visitMethodInvocation(mi, foundDelegates); + } + }.reduce(method.getBody(), new HashSet<>()); + return foundDelegates.size() == 1 ? foundDelegates.iterator().next() : null; + } + + private @Nullable String findNullCheckVariable(J.MethodDeclaration method) { + if (method.getBody() == null) { + return null; + } + + List statements = method.getBody().getStatements(); + + // Check for if-statement null check pattern: if (x == null) return -1; return x.read(); + if (statements.size() == 2 && statements.get(0) instanceof J.If) { + J.If ifStmt = (J.If) statements.get(0); + String nullVar = extractNullCheckVariable(ifStmt.getIfCondition().getTree()); + if (nullVar != null) { + return nullVar; + } + } + + // Check for ternary null check pattern: return x == null ? -1 : x.read(); + for (Statement stmt : statements) { + if (!(stmt instanceof J.Return)) { + continue; + } + J.Return ret = (J.Return) stmt; + Expression expr = ret.getExpression(); + + if (!(expr instanceof J.Ternary)) { + continue; + } + J.Ternary ternary = (J.Ternary) expr; + String nullVar = extractNullCheckVariable(ternary.getCondition()); + if (nullVar != null) { + return nullVar; + } + } + return null; + } + + private @Nullable String extractNullCheckVariable(Expression condition) { + if (!(condition instanceof J.Binary)) { + return null; + } + J.Binary binary = (J.Binary) condition; + if (binary.getOperator() != J.Binary.Type.Equal) { + return null; + } + + Expression left = binary.getLeft(); + Expression right = binary.getRight(); + + if (J.Literal.isLiteralValue(right, null) && left instanceof J.Identifier) { + return ((J.Identifier) left).getSimpleName(); + } + if (J.Literal.isLiteralValue(left, null) && right instanceof J.Identifier) { + return ((J.Identifier) right).getSimpleName(); + } + return null; + } + + private Statement createBulkReadMethod(String delegate, boolean hasNullCheck, boolean usesIfStyle, J.Block body) { + String bulkReadMethod; + if (hasNullCheck) { + if (usesIfStyle) { + bulkReadMethod = String.format( + "@Override\n" + + "public int read(byte[] b, int off, int len) throws IOException {\n" + + " if (%s == null) {\n" + + " return -1;\n" + + " }\n" + + " return %s.read(b, off, len);\n" + + "}", + delegate, delegate); + } else { + bulkReadMethod = String.format( + "@Override\n" + + "public int read(byte[] b, int off, int len) throws IOException {\n" + + " return %s == null ? -1 : %s.read(b, off, len);\n" + + "}", + delegate, delegate); + } + } else { + bulkReadMethod = String.format( + "@Override\n" + + "public int read(byte[] b, int off, int len) throws IOException {\n" + + " return %s.read(b, off, len);\n" + + "}", + delegate); + } + + JavaTemplate template = JavaTemplate.builder(bulkReadMethod) + .contextSensitive() + .imports("java.io.IOException") + .build(); + + J.Block newBody = body.withStatements(emptyList()); + J.Block withNewMethod = template.apply( + new Cursor(getCursor(), newBody), + newBody.getCoordinates().lastStatement()); + Statement bulkMethod = withNewMethod.getStatements().get(0); + + // Add blank line before the new method + String existingWhitespace = bulkMethod.getPrefix().getWhitespace(); + return bulkMethod.withPrefix(Space.format("\n" + existingWhitespace)); + } + }); + } + + @Value + private static class AnalysisResult { + J.MethodDeclaration readMethod; + boolean hasBulkRead; + + @Nullable + String delegate; + + boolean hasNullCheck; + boolean usesIfStyle; + boolean complex; + } +} diff --git a/src/main/resources/META-INF/rewrite/examples.yml b/src/main/resources/META-INF/rewrite/examples.yml index 824edb46c3..d24f8982ce 100644 --- a/src/main/resources/META-INF/rewrite/examples.yml +++ b/src/main/resources/META-INF/rewrite/examples.yml @@ -4273,6 +4273,50 @@ examples: language: java --- type: specs.openrewrite.org/v1beta/example +recipeName: org.openrewrite.java.migrate.io.AddInputStreamBulkReadMethod +examples: +- description: '`Transform#anonymousClassWithSimpleDelegation`' + sources: + - before: | + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream delegate; + + InputStream getWrappedStream() { + return new InputStream() { + @Override + public int read() throws IOException { + return delegate.read(); + } + }; + } + } + after: | + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream delegate; + + InputStream getWrappedStream() { + return new InputStream() { + @Override + public int read() throws IOException { + return delegate.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return delegate.read(b, off, len); + } + }; + } + } + language: java +--- +type: specs.openrewrite.org/v1beta/example recipeName: org.openrewrite.java.migrate.io.ReplaceFileInOrOutputStreamFinalizeWithClose examples: - description: '`ReplaceFileInOrOutputStreamFinalizeWithCloseTest#removeFinalizerForFileInputStream`' diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index c031117ed2..c5758a53c7 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -151,6 +151,7 @@ maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.g maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.guava.PreferMathSubtractExact,Prefer `Math#subtractExact`,Prefer `java.lang.Math#subtractExact` instead of using `com.google.common.primitives.IntMath#checkedSubtract` or `com.google.common.primitives.IntMath#subtractExact`.,5,,Guava,Modernize,Java,,Recipes for migrating from [Google Guava](https://github.com/google/guava) to Java standard library.,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.guava.PreferMathMultiplyExact,Prefer `Math#multiplyExact`,Prefer `java.lang.Math#multiplyExact` instead of using `com.google.common.primitives.IntMath#checkedMultiply` or `com.google.common.primitives.IntMath#multiplyExact`.,5,,Guava,Modernize,Java,,Recipes for migrating from [Google Guava](https://github.com/google/guava) to Java standard library.,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.guava.PreferMathClamp,Prefer `Math#clamp`,Prefer `java.lang.Math#clamp` instead of using `com.google.common.primitives.*#constrainToRange`.,13,,Guava,Modernize,Java,,Recipes for migrating from [Google Guava](https://github.com/google/guava) to Java standard library.,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.io.AddInputStreamBulkReadMethod,Add bulk read method to `InputStream` implementations,"Adds a `read(byte[], int, int)` method to `InputStream` subclasses that only override the single-byte `read()` method. Java's default `InputStream.read(byte[], int, int)` implementation calls the single-byte `read()` method in a loop, which can cause severe performance degradation (up to 350x slower) for bulk reads. This recipe detects `InputStream` implementations that delegate to another stream and adds the missing bulk read method to delegate bulk reads as well.",1,,`java.io` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.io.ReplaceFileInOrOutputStreamFinalizeWithClose,Replace invocations of `finalize()` on `FileInputStream` and `FileOutputStream` with `close()`,Replace invocations of the deprecated `finalize()` method on `FileInputStream` and `FileOutputStream` with `close()`.,1,,`java.io` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.io.ReplaceSystemOutWithIOPrint,Migrate `System.out.print` to Java 25 IO utility class,"Replace `System.out.print()`, `System.out.println()` with `IO.print()` and `IO.println()`. Migrates to the new IO utility class introduced in Java 25.",1,,`java.io` APIs,Modernize,Java,,,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-migrate-java,org.openrewrite.java.migrate.jakarta.ApplicationPathWildcardNoLongerAccepted,Remove trailing slash from `jakarta.ws.rs.ApplicationPath` values,Remove trailing `/*` from `jakarta.ws.rs.ApplicationPath` values.,1,,Jakarta,Modernize,Java,,Recipes for migrating to [Jakarta EE](https://jakarta.ee/).,Modernize your code to best use the project's current JDK version. Take advantage of newly available APIs and reduce the dependency of your code on third party dependencies where there is equivalent functionality in the Java standard library.,Basic building blocks for transforming Java code.,, diff --git a/src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java b/src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java new file mode 100644 index 0000000000..1556b93122 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java @@ -0,0 +1,1082 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.migrate.io; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; + +class AddInputStreamBulkReadMethodTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new AddInputStreamBulkReadMethod()); + } + + @Nested + class Transform { + + @DocumentExample + @Test + void anonymousClassWithSimpleDelegation() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream delegate; + + InputStream getWrappedStream() { + return new InputStream() { + @Override + public int read() throws IOException { + return delegate.read(); + } + }; + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream delegate; + + InputStream getWrappedStream() { + return new InputStream() { + @Override + public int read() throws IOException { + return delegate.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return delegate.read(b, off, len); + } + }; + } + } + """ + ) + ); + } + + @Test + void anonymousClassWithNullCheck() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream body; + + InputStream getBody() { + return new InputStream() { + @Override + public int read() throws IOException { + return body == null ? -1 : body.read(); + } + }; + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream body; + + InputStream getBody() { + return new InputStream() { + @Override + public int read() throws IOException { + return body == null ? -1 : body.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return body == null ? -1 : body.read(b, off, len); + } + }; + } + } + """ + ) + ); + } + + @Test + void anonymousClassWithNullCheckReversed() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream body; + + InputStream getBody() { + return new InputStream() { + @Override + public int read() throws IOException { + return null == body ? -1 : body.read(); + } + }; + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream body; + + InputStream getBody() { + return new InputStream() { + @Override + public int read() throws IOException { + return null == body ? -1 : body.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return body == null ? -1 : body.read(b, off, len); + } + }; + } + } + """ + ) + ); + } + + @Test + void localVariableDelegate() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + InputStream wrap(InputStream source) { + return new InputStream() { + @Override + public int read() throws IOException { + return source.read(); + } + }; + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + InputStream wrap(InputStream source) { + return new InputStream() { + @Override + public int read() throws IOException { + return source.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return source.read(b, off, len); + } + }; + } + } + """ + ) + ); + } + + @Test + void anonymousClassWithCloseMethod() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream body; + private Runnable onClose; + + InputStream getBody() { + return new InputStream() { + @Override + public int read() throws IOException { + return body == null ? -1 : body.read(); + } + + @Override + public void close() throws IOException { + if (body != null) body.close(); + onClose.run(); + } + }; + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream body; + private Runnable onClose; + + InputStream getBody() { + return new InputStream() { + @Override + public int read() throws IOException { + return body == null ? -1 : body.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return body == null ? -1 : body.read(b, off, len); + } + + @Override + public void close() throws IOException { + if (body != null) body.close(); + onClose.run(); + } + }; + } + } + """ + ) + ); + } + + @Test + void namedClassWithSimpleDelegation() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class DelegatingInputStream extends InputStream { + private final InputStream delegate; + + DelegatingInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + return delegate.read(); + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class DelegatingInputStream extends InputStream { + private final InputStream delegate; + + DelegatingInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + return delegate.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return delegate.read(b, off, len); + } + } + """ + ) + ); + } + + @Test + void namedClassWithNullCheck() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class SafeInputStream extends InputStream { + private InputStream delegate; + + void setDelegate(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + return delegate == null ? -1 : delegate.read(); + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class SafeInputStream extends InputStream { + private InputStream delegate; + + void setDelegate(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + return delegate == null ? -1 : delegate.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return delegate == null ? -1 : delegate.read(b, off, len); + } + } + """ + ) + ); + } + + @Test + void anonymousClassWithIfNullCheck() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream body; + + InputStream getBody() { + return new InputStream() { + @Override + public int read() throws IOException { + if (body == null) { + return -1; + } + return body.read(); + } + }; + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream body; + + InputStream getBody() { + return new InputStream() { + @Override + public int read() throws IOException { + if (body == null) { + return -1; + } + return body.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (body == null) { + return -1; + } + return body.read(b, off, len); + } + }; + } + } + """ + ) + ); + } + + @Test + void namedClassWithIfNullCheck() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class SafeInputStream extends InputStream { + private InputStream delegate; + + void setDelegate(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + if (delegate == null) { + return -1; + } + return delegate.read(); + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class SafeInputStream extends InputStream { + private InputStream delegate; + + void setDelegate(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + if (delegate == null) { + return -1; + } + return delegate.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (delegate == null) { + return -1; + } + return delegate.read(b, off, len); + } + } + """ + ) + ); + } + + @Test + void delegateIsInputStreamSubclass() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + import java.util.zip.ZipInputStream; + + class Example { + private ZipInputStream zipIn; + + InputStream getWrappedStream() { + return new InputStream() { + @Override + public int read() throws IOException { + return zipIn.read(); + } + }; + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + import java.util.zip.ZipInputStream; + + class Example { + private ZipInputStream zipIn; + + InputStream getWrappedStream() { + return new InputStream() { + @Override + public int read() throws IOException { + return zipIn.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return zipIn.read(b, off, len); + } + }; + } + } + """ + ) + ); + } + + @Test + void fieldAccessWithThis() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream delegate; + + InputStream getWrappedStream() { + return new InputStream() { + @Override + public int read() throws IOException { + return Example.this.delegate.read(); + } + }; + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream delegate; + + InputStream getWrappedStream() { + return new InputStream() { + @Override + public int read() throws IOException { + return Example.this.delegate.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return Example.this.delegate.read(b, off, len); + } + }; + } + } + """ + ) + ); + } + } + + @Nested + class NoChange { + + @Test + void alreadyHasBulkReadMethod() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream delegate; + + InputStream getWrappedStream() { + return new InputStream() { + @Override + public int read() throws IOException { + return delegate.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return delegate.read(b, off, len); + } + }; + } + } + """ + ) + ); + } + + @Test + void notExtendingInputStream() { + rewriteRun( + java( + """ + import java.io.IOException; + + class Example { + void foo() { + Runnable r = new Runnable() { + @Override + public void run() { + } + }; + } + } + """ + ) + ); + } + + @Test + void filterInputStreamSubclass() { + rewriteRun( + java( + """ + import java.io.FilterInputStream; + import java.io.IOException; + import java.io.InputStream; + + class MyFilterStream extends FilterInputStream { + MyFilterStream(InputStream in) { + super(in); + } + + @Override + public int read() throws IOException { + return in.read(); + } + } + """ + ) + ); + } + + @Test + void namedClassAlreadyHasBulkRead() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class DelegatingInputStream extends InputStream { + private final InputStream delegate; + + DelegatingInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + return delegate.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return delegate.read(b, off, len); + } + } + """ + ) + ); + } + + } + + @Nested + class MarkForReview { + // These tests verify that complex cases with identifiable delegates + // get a search marker so developers can manually review them + + @Test + void marksComplexBodyWithSideEffectsForReview() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class CountingInputStream extends InputStream { + private final InputStream delegate; + private long bytesRead = 0; + + CountingInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + int b = delegate.read(); + if (b != -1) bytesRead++; + return b; + } + + public long getBytesRead() { + return bytesRead; + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + /*~~(Missing bulk read method may cause significant performance degradation)~~>*/class CountingInputStream extends InputStream { + private final InputStream delegate; + private long bytesRead = 0; + + CountingInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + int b = delegate.read(); + if (b != -1) bytesRead++; + return b; + } + + public long getBytesRead() { + return bytesRead; + } + } + """ + ) + ); + } + + @Test + void marksAnonymousComplexBodyForReview() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream delegate; + private long count = 0; + + InputStream getWrappedStream() { + return new InputStream() { + @Override + public int read() throws IOException { + int b = delegate.read(); + if (b != -1) count++; + return b; + } + }; + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream delegate; + private long count = 0; + + InputStream getWrappedStream() { + return /*~~(Missing bulk read method may cause significant performance degradation)~~>*/new InputStream() { + @Override + public int read() throws IOException { + int b = delegate.read(); + if (b != -1) count++; + return b; + } + }; + } + } + """ + ) + ); + } + + @Test + void marksComplexBodyWithMultipleStatementsForReview() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class LoggingInputStream extends InputStream { + private final InputStream delegate; + + LoggingInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + System.out.println("Reading byte"); + return delegate.read(); + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + /*~~(Missing bulk read method may cause significant performance degradation)~~>*/class LoggingInputStream extends InputStream { + private final InputStream delegate; + + LoggingInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + System.out.println("Reading byte"); + return delegate.read(); + } + } + """ + ) + ); + } + + @Test + void marksComplexBodyWithTransformationForReview() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class XorInputStream extends InputStream { + private final InputStream delegate; + private final int xorKey; + + XorInputStream(InputStream delegate, int xorKey) { + this.delegate = delegate; + this.xorKey = xorKey; + } + + @Override + public int read() throws IOException { + int b = delegate.read(); + return b == -1 ? -1 : (b ^ xorKey); + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + /*~~(Missing bulk read method may cause significant performance degradation)~~>*/class XorInputStream extends InputStream { + private final InputStream delegate; + private final int xorKey; + + XorInputStream(InputStream delegate, int xorKey) { + this.delegate = delegate; + this.xorKey = xorKey; + } + + @Override + public int read() throws IOException { + int b = delegate.read(); + return b == -1 ? -1 : (b ^ xorKey); + } + } + """ + ) + ); + } + + @Test + void marksComplexBodyWithTryCatchForReview() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class SafeInputStream extends InputStream { + private final InputStream delegate; + + SafeInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + try { + return delegate.read(); + } catch (IOException e) { + return -1; + } + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + /*~~(Missing bulk read method may cause significant performance degradation)~~>*/class SafeInputStream extends InputStream { + private final InputStream delegate; + + SafeInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public int read() throws IOException { + try { + return delegate.read(); + } catch (IOException e) { + return -1; + } + } + } + """ + ) + ); + } + + @Test + void marksNoDelegateForManualImplementation() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + InputStream getStream() { + return new InputStream() { + private int position = 0; + private byte[] data = new byte[100]; + + @Override + public int read() throws IOException { + if (position >= data.length) return -1; + return data[position++] & 0xff; + } + }; + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + InputStream getStream() { + return /*~~(Missing bulk read method may cause significant performance degradation)~~>*/new InputStream() { + private int position = 0; + private byte[] data = new byte[100]; + + @Override + public int read() throws IOException { + if (position >= data.length) return -1; + return data[position++] & 0xff; + } + }; + } + } + """ + ) + ); + } + + @Test + void marksConditionalDelegationForManualImplementation() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class ConditionalInputStream extends InputStream { + private final InputStream primary; + private final InputStream fallback; + private boolean usePrimary = true; + + ConditionalInputStream(InputStream primary, InputStream fallback) { + this.primary = primary; + this.fallback = fallback; + } + + @Override + public int read() throws IOException { + return usePrimary ? primary.read() : fallback.read(); + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + /*~~(Missing bulk read method may cause significant performance degradation)~~>*/class ConditionalInputStream extends InputStream { + private final InputStream primary; + private final InputStream fallback; + private boolean usePrimary = true; + + ConditionalInputStream(InputStream primary, InputStream fallback) { + this.primary = primary; + this.fallback = fallback; + } + + @Override + public int read() throws IOException { + return usePrimary ? primary.read() : fallback.read(); + } + } + """ + ) + ); + } + + @Test + void markAnonymousClassWithQualifiedNullCheckForReview() { + rewriteRun( + java( + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream body; + + InputStream getBody() { + return new InputStream() { + @Override + public int read() throws IOException { + return Example.this.body == null ? -1 : Example.this.body.read(); + } + }; + } + } + """, + """ + import java.io.IOException; + import java.io.InputStream; + + class Example { + private InputStream body; + + InputStream getBody() { + return /*~~(Missing bulk read method may cause significant performance degradation)~~>*/new InputStream() { + @Override + public int read() throws IOException { + return Example.this.body == null ? -1 : Example.this.body.read(); + } + }; + } + } + """ + ) + ); + } + } +}