From 1b829270f4bdab4fecc1b863f1641558242ff999 Mon Sep 17 00:00:00 2001 From: Peter Streef Date: Mon, 5 Jan 2026 13:09:14 +0100 Subject: [PATCH 1/6] Add bulk read method to InputStream implementations Adds a recipe to detect and fix InputStream subclasses that only override the single-byte read() method. Java's default bulk read implementation calls read() in a loop, which can cause up to 350x slower performance. For simple delegation patterns, the recipe adds the missing bulk read method. For complex bodies (side effects, transformations), it adds a search marker for manual review. Moved from openrewrite/rewrite#6383 per review feedback. --- .../io/AddInputStreamBulkReadMethod.java | 499 ++++++++ .../io/AddInputStreamBulkReadMethodTest.java | 1040 +++++++++++++++++ 2 files changed, 1539 insertions(+) create mode 100644 src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java create mode 100644 src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java 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..6a2e335e97 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java @@ -0,0 +1,499 @@ +/* + * 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.Cursor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.JavaTemplate; +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; +import static java.util.Collections.singletonList; + +public class AddInputStreamBulkReadMethod extends Recipe { + + private static final String MARKER_MESSAGE = "Missing bulk read method may cause significant performance degradation"; + + @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 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.InputStream", 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.InputStream", 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 (T) SearchResult.found(tree, MARKER_MESSAGE); + } + + // Simple delegation - add bulk read method + Statement bulkMethod = createBulkReadMethod(result.getDelegate(), result.isHasNullCheck(), result.isUsesIfStyle(), body); + if (bulkMethod == null) { + return tree; + } + + J.MethodDeclaration targetMethod = result.getReadMethod(); + J.Block newBody = body.withStatements( + ListUtils.flatMap(body.getStatements(), stmt -> { + if (stmt == targetMethod) { + return Arrays.asList(stmt, bulkMethod); + } + return singletonList(stmt); + }) + ); + + if (tree instanceof J.ClassDeclaration) { + return (T) ((J.ClassDeclaration) tree).withBody(newBody); + } else 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(); + boolean leftIsNull = left instanceof J.Literal && ((J.Literal) left).getValue() == null; + boolean rightIsNull = right instanceof J.Literal && ((J.Literal) right).getValue() == null; + boolean leftIsIdent = left instanceof J.Identifier; + boolean rightIsIdent = right instanceof J.Identifier; + return (leftIsNull && rightIsIdent) || (rightIsNull && leftIsIdent); + } + + 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.InputStream", mi.getSelect().getType())) { + continue; + } + + // Get the delegate name + if (mi.getSelect() instanceof J.Identifier) { + return ((J.Identifier) mi.getSelect()).getSimpleName(); + } else 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 HashSet<>(); + new JavaIsoVisitor() { + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, Integer p) { + if ("read".equals(mi.getSimpleName()) && + mi.getSelect() != null && + TypeUtils.isAssignableTo("java.io.InputStream", 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, p); + } + }.visit(method.getBody(), 0); + + 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 (right instanceof J.Literal && ((J.Literal) right).getValue() == null) { + if (left instanceof J.Identifier) { + return ((J.Identifier) left).getSimpleName(); + } + } + if (left instanceof J.Literal && ((J.Literal) left).getValue() == null) { + if (right instanceof J.Identifier) { + return ((J.Identifier) right).getSimpleName(); + } + } + return null; + } + + private @Nullable 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; + } +} \ No newline at end of file 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..57fdae753c --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java @@ -0,0 +1,1040 @@ +/* + * 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(); + } + } + """ + ) + ); + } + } +} \ No newline at end of file From b6cfa16d9f6fc84e91d301098e3b85903d422378 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 5 Jan 2026 13:22:45 +0100 Subject: [PATCH 2/6] Apply the first automated suggestions --- .../io/AddInputStreamBulkReadMethod.java | 80 +++++++++---------- .../io/AddInputStreamBulkReadMethodTest.java | 2 +- 2 files changed, 39 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java b/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java index 6a2e335e97..5e216bc675 100644 --- a/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java +++ b/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java @@ -47,16 +47,15 @@ public String getDisplayName() { @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."; + "`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 new JavaIsoVisitor() { - @Override public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); @@ -145,7 +144,7 @@ private T processInputStreamClass(T tree, J.Block body) { continue; } if (method.getParameters().isEmpty() || - (method.getParameters().size() == 1 && method.getParameters().get(0) instanceof J.Empty)) { + (method.getParameters().size() == 1 && method.getParameters().get(0) instanceof J.Empty)) { readMethod = method; } else if (method.getParameters().size() == 3) { hasBulkRead = true; @@ -218,7 +217,7 @@ private boolean isSimpleReturnStatement(Statement stmt) { } // Check true part is a literal (like -1) if (!(ternary.getTruePart() instanceof J.Literal) && - !(ternary.getTruePart() instanceof J.Unary)) { + !(ternary.getTruePart() instanceof J.Unary)) { return false; } expr = ternary.getFalsePart(); @@ -253,7 +252,7 @@ private boolean isNullCheckIfPattern(Statement first, Statement second) { } J.Return thenReturn = (J.Return) thenStmt; if (!(thenReturn.getExpression() instanceof J.Literal) && - !(thenReturn.getExpression() instanceof J.Unary)) { + !(thenReturn.getExpression() instanceof J.Unary)) { return false; } @@ -282,7 +281,7 @@ private boolean isSimpleReadInvocation(@Nullable Expression expr) { // Should have no arguments (single-byte read) if (!mi.getArguments().isEmpty() && - !(mi.getArguments().size() == 1 && mi.getArguments().get(0) instanceof J.Empty)) { + !(mi.getArguments().size() == 1 && mi.getArguments().get(0) instanceof J.Empty)) { return false; } @@ -299,8 +298,8 @@ private boolean isSimpleNullCheck(Expression condition) { } Expression left = binary.getLeft(); Expression right = binary.getRight(); - boolean leftIsNull = left instanceof J.Literal && ((J.Literal) left).getValue() == null; - boolean rightIsNull = right instanceof J.Literal && ((J.Literal) right).getValue() == null; + boolean leftIsNull = J.Literal.isLiteralValue(left, null); + boolean rightIsNull = J.Literal.isLiteralValue(right, null); boolean leftIsIdent = left instanceof J.Identifier; boolean rightIsIdent = right instanceof J.Identifier; return (leftIsNull && rightIsIdent) || (rightIsNull && leftIsIdent); @@ -358,23 +357,21 @@ private boolean isSimpleNullCheck(Expression condition) { return null; } - Set foundDelegates = new HashSet<>(); - new JavaIsoVisitor() { + Set foundDelegates = new JavaIsoVisitor>() { @Override - public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, Integer p) { + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, Set foundDelegates) { if ("read".equals(mi.getSimpleName()) && - mi.getSelect() != null && - TypeUtils.isAssignableTo("java.io.InputStream", mi.getSelect().getType())) { + mi.getSelect() != null && + TypeUtils.isAssignableTo("java.io.InputStream", 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, p); + return super.visitMethodInvocation(mi, foundDelegates); } - }.visit(method.getBody(), 0); - + }.reduce(method.getBody(), new HashSet<>()); return foundDelegates.size() == 1 ? foundDelegates.iterator().next() : null; } @@ -426,46 +423,42 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, Integer p Expression left = binary.getLeft(); Expression right = binary.getRight(); - if (right instanceof J.Literal && ((J.Literal) right).getValue() == null) { - if (left instanceof J.Identifier) { - return ((J.Identifier) left).getSimpleName(); - } + if (J.Literal.isLiteralValue(right, null) && left instanceof J.Identifier) { + return ((J.Identifier) left).getSimpleName(); } - if (left instanceof J.Literal && ((J.Literal) left).getValue() == null) { - if (right instanceof J.Identifier) { - return ((J.Identifier) right).getSimpleName(); - } + if (J.Literal.isLiteralValue(left, null) && right instanceof J.Identifier) { + return ((J.Identifier) right).getSimpleName(); } return null; } - private @Nullable Statement createBulkReadMethod(String delegate, boolean hasNullCheck, boolean usesIfStyle, J.Block body) { + 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" + - "}", + "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" + - "}", + "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" + - "}", + "public int read(byte[] b, int off, int len) throws IOException {\n" + + " return %s.read(b, off, len);\n" + + "}", delegate); } @@ -491,9 +484,12 @@ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, Integer p private static class AnalysisResult { J.MethodDeclaration readMethod; boolean hasBulkRead; - @Nullable String delegate; + + @Nullable + String delegate; + boolean hasNullCheck; boolean usesIfStyle; boolean complex; } -} \ No newline at end of file +} diff --git a/src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java b/src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java index 57fdae753c..1cf2371a9e 100644 --- a/src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java +++ b/src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java @@ -1037,4 +1037,4 @@ public int read() throws IOException { ); } } -} \ No newline at end of file +} From e1283ff2da1177dbfc00412036063c2cbd0087cc Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 5 Jan 2026 13:27:08 +0100 Subject: [PATCH 3/6] Add line to recipes.csv --- src/main/resources/META-INF/rewrite/recipes.csv | 1 + 1 file changed, 1 insertion(+) 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.,, From dda69a194863e6f2a48e9005c440719ffaf3b1c2 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 5 Jan 2026 13:30:49 +0100 Subject: [PATCH 4/6] Apply formatter and further best practices --- .../io/AddInputStreamBulkReadMethod.java | 12 +++-- .../resources/META-INF/rewrite/examples.yml | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java b/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java index 5e216bc675..eb8e4636b6 100644 --- a/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java +++ b/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java @@ -104,15 +104,11 @@ private T processInputStreamClass(T tree, J.Block body) { // No delegate found or complex body - add marker for manual implementation if (result.getDelegate() == null || result.isComplex()) { - return (T) SearchResult.found(tree, MARKER_MESSAGE); + return SearchResult.found(tree, MARKER_MESSAGE); } // Simple delegation - add bulk read method Statement bulkMethod = createBulkReadMethod(result.getDelegate(), result.isHasNullCheck(), result.isUsesIfStyle(), body); - if (bulkMethod == null) { - return tree; - } - J.MethodDeclaration targetMethod = result.getReadMethod(); J.Block newBody = body.withStatements( ListUtils.flatMap(body.getStatements(), stmt -> { @@ -125,7 +121,8 @@ private T processInputStreamClass(T tree, J.Block body) { if (tree instanceof J.ClassDeclaration) { return (T) ((J.ClassDeclaration) tree).withBody(newBody); - } else if (tree instanceof J.NewClass) { + } + if (tree instanceof J.NewClass) { return (T) ((J.NewClass) tree).withBody(newBody); } return tree; @@ -339,7 +336,8 @@ private boolean isSimpleNullCheck(Expression condition) { // Get the delegate name if (mi.getSelect() instanceof J.Identifier) { return ((J.Identifier) mi.getSelect()).getSimpleName(); - } else if (mi.getSelect() instanceof J.FieldAccess) { + } + if (mi.getSelect() instanceof J.FieldAccess) { J.FieldAccess fa = (J.FieldAccess) mi.getSelect(); return fa.print(getCursor()); } diff --git a/src/main/resources/META-INF/rewrite/examples.yml b/src/main/resources/META-INF/rewrite/examples.yml index 29bad41ba0..98cc115f85 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`' From 973e9434dbcdc276db7078e12daaf8df180634a6 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 5 Jan 2026 13:46:28 +0100 Subject: [PATCH 5/6] Add placeholder for expanded precondition support upstream --- .../io/AddInputStreamBulkReadMethod.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java b/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java index eb8e4636b6..35883fc652 100644 --- a/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java +++ b/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java @@ -17,13 +17,11 @@ import lombok.Value; import org.jspecify.annotations.Nullable; -import org.openrewrite.Cursor; -import org.openrewrite.ExecutionContext; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; +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; @@ -38,6 +36,7 @@ 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() { @@ -55,13 +54,14 @@ public String getDescription() { @Override public TreeVisitor getVisitor() { - return new JavaIsoVisitor() { + DeclaresType precondition = new DeclaresType<>(JAVA_IO_INPUT_STREAM, true); + return Preconditions.check(/*TODO precondition*/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.InputStream", cd.getType())) { + if (cd.getExtends() == null || !TypeUtils.isAssignableTo(JAVA_IO_INPUT_STREAM, cd.getType())) { return cd; } @@ -83,7 +83,7 @@ public J.NewClass visitNewClass(J.NewClass newClass, ExecutionContext ctx) { } // Must extend InputStream - if (!TypeUtils.isAssignableTo("java.io.InputStream", nc.getType())) { + if (!TypeUtils.isAssignableTo(JAVA_IO_INPUT_STREAM, nc.getType())) { return nc; } @@ -329,7 +329,7 @@ private boolean isSimpleNullCheck(Expression condition) { } // The select must be an InputStream - if (!TypeUtils.isAssignableTo("java.io.InputStream", mi.getSelect().getType())) { + if (!TypeUtils.isAssignableTo(JAVA_IO_INPUT_STREAM, mi.getSelect().getType())) { continue; } @@ -360,7 +360,7 @@ private boolean isSimpleNullCheck(Expression condition) { public J.MethodInvocation visitMethodInvocation(J.MethodInvocation mi, Set foundDelegates) { if ("read".equals(mi.getSimpleName()) && mi.getSelect() != null && - TypeUtils.isAssignableTo("java.io.InputStream", mi.getSelect().getType())) { + 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) { @@ -475,7 +475,7 @@ private Statement createBulkReadMethod(String delegate, boolean hasNullCheck, bo String existingWhitespace = bulkMethod.getPrefix().getWhitespace(); return bulkMethod.withPrefix(Space.format("\n" + existingWhitespace)); } - }; + }); } @Value From 5f9b6efdb56306afeb8df23905fe58aef02d76f9 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Mon, 5 Jan 2026 14:32:53 +0100 Subject: [PATCH 6/6] Add precondition and inline conditionals logic --- .../io/AddInputStreamBulkReadMethod.java | 26 ++++-------- .../io/AddInputStreamBulkReadMethodTest.java | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java b/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java index 35883fc652..4f6dc9d49e 100644 --- a/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java +++ b/src/main/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethod.java @@ -31,7 +31,6 @@ import java.util.Set; import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; public class AddInputStreamBulkReadMethod extends Recipe { @@ -54,8 +53,7 @@ public String getDescription() { @Override public TreeVisitor getVisitor() { - DeclaresType precondition = new DeclaresType<>(JAVA_IO_INPUT_STREAM, true); - return Preconditions.check(/*TODO precondition*/true, new JavaIsoVisitor() { + 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); @@ -107,17 +105,10 @@ private T processInputStreamClass(T tree, J.Block body) { return SearchResult.found(tree, MARKER_MESSAGE); } - // Simple delegation - add bulk read method + // Simple delegation - add bulk read method right after the single-byte read method Statement bulkMethod = createBulkReadMethod(result.getDelegate(), result.isHasNullCheck(), result.isUsesIfStyle(), body); - J.MethodDeclaration targetMethod = result.getReadMethod(); - J.Block newBody = body.withStatements( - ListUtils.flatMap(body.getStatements(), stmt -> { - if (stmt == targetMethod) { - return Arrays.asList(stmt, bulkMethod); - } - return singletonList(stmt); - }) - ); + 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); @@ -295,11 +286,10 @@ private boolean isSimpleNullCheck(Expression condition) { } Expression left = binary.getLeft(); Expression right = binary.getRight(); - boolean leftIsNull = J.Literal.isLiteralValue(left, null); - boolean rightIsNull = J.Literal.isLiteralValue(right, null); - boolean leftIsIdent = left instanceof J.Identifier; - boolean rightIsIdent = right instanceof J.Identifier; - return (leftIsNull && rightIsIdent) || (rightIsNull && leftIsIdent); + 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) { diff --git a/src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java b/src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java index 1cf2371a9e..1556b93122 100644 --- a/src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java +++ b/src/test/java/org/openrewrite/java/migrate/io/AddInputStreamBulkReadMethodTest.java @@ -1036,5 +1036,47 @@ public int read() throws IOException { ) ); } + + @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(); + } + }; + } + } + """ + ) + ); + } } }