Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a21ff07
Add `ReplaceDeprecatedKotlinMethod` with `template` argument
timtebeek Jan 17, 2026
51041ac
Update recipes.csv
timtebeek Jan 18, 2026
b70b120
Move and add scanner/generator for faster iterations
timtebeek Jan 24, 2026
83de309
Merge branch 'main' into replace-deprecated-kotlin-methods
timtebeek Jan 24, 2026
0b66606
Improve the scanner, generator and replacement recipe
timtebeek Jan 24, 2026
8e75c0d
Apply suggestions from code review
timtebeek Jan 24, 2026
51ca8f9
Apply suggestions from code review
timtebeek Jan 24, 2026
a820940
Move filter to scanner; keep Kotlin types and use `*` for generic types
timtebeek Jan 24, 2026
364226d
Handle extension functions and receivers
timtebeek Jan 24, 2026
5fb24c8
Add comments to show original expression
timtebeek Jan 24, 2026
aee2c6d
Apply suggestions from code review
timtebeek Jan 24, 2026
29b8a9e
Special handling for suspend and coroutines
timtebeek Jan 24, 2026
58ac501
Add recipes for Kotlinx
timtebeek Jan 24, 2026
4525d2c
Polish `ReplaceDeprecatedKotlinMethod`
timtebeek Jan 24, 2026
f3a3ef1
Also support replacements for constructors
timtebeek Jan 24, 2026
ff456ea
Rename recipe
timtebeek Jan 24, 2026
75f43f5
Add a test for constructor replacement
timtebeek Jan 24, 2026
0add258
Quick renames
timtebeek Jan 24, 2026
090f83f
Expect explicit groupId passed in
timtebeek Jan 24, 2026
86a26a8
Apply formatter
timtebeek Jan 24, 2026
7d5c0f3
Polish DeprecatedMethodScanner
timtebeek Jan 24, 2026
eb2cce6
Collapse catch blocks
timtebeek Jan 24, 2026
7c6a578
Use `<init>`
timtebeek Jan 27, 2026
fc59b9c
Merge branch 'main' into replace-deprecated-kotlin-methods
timtebeek Feb 12, 2026
d721c4e
Exclude transitive kotlin-stdlib from kotlin-metadata-jvm (#6726)
timtebeek Feb 12, 2026
d5fd61b
Comment out the testRuntime dependencies when not generating
timtebeek Feb 13, 2026
58a95a6
Increase heap size
timtebeek Feb 13, 2026
7773471
Merge branch 'main' into replace-deprecated-kotlin-methods
timtebeek Feb 13, 2026
90f971c
Update recipes.csv
timtebeek Feb 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions rewrite-kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ dependencies {
implementation(kotlin("compiler-embeddable", kotlinVersion))
implementation(kotlin("stdlib", kotlinVersion))

testImplementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.1.0")

testImplementation("org.junit-pioneer:junit-pioneer:latest.release")
testImplementation(project(":rewrite-test"))
testRuntimeOnly(project(":rewrite-java-21"))
Expand All @@ -27,8 +29,19 @@ dependencies {
testImplementation("com.github.ajalt.clikt:clikt:3.5.0")
testImplementation("com.squareup:javapoet:1.13.0")
testImplementation("com.google.testing.compile:compile-testing:0.+")

// Kotlin libraries for KotlinDeprecationRecipeGenerator
testRuntimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.+")
testRuntimeOnly("org.jetbrains.kotlinx:kotlinx-serialization-core:1.+")
}

recipeDependencies {
// Kotlin libraries with @Deprecated(replaceWith=ReplaceWith(...)) annotations
testParserClasspath("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.+")
testParserClasspath("org.jetbrains.kotlinx:kotlinx-serialization-core:1.+")
}


java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
Expand All @@ -42,3 +55,14 @@ tasks.withType<KotlinCompile>().configureEach {
jvmTarget.set(if (name.contains("Test")) JvmTarget.JVM_21 else JvmTarget.JVM_1_8)
}
}

tasks {
val generateKotlinDeprecationRecipes by registering(JavaExec::class) {
group = "generate"
description = "Generate recipes from Kotlin @Deprecated annotations with ReplaceWith."
mainClass = "org.openrewrite.kotlin.replace.KotlinDeprecationRecipeGenerator"
classpath = sourceSets.getByName("test").runtimeClasspath
args("arrow-core", "kotlinx-coroutines-core", "kotlinx-serialization-core")
finalizedBy("licenseFormat")
}
}
Comment thread
timtebeek marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
/*
* Copyright 2026 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.kotlin.replace;

import lombok.EqualsAndHashCode;
import lombok.Value;
import org.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.java.MethodMatcher;
import org.openrewrite.java.search.UsesMethod;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
import org.openrewrite.kotlin.KotlinParser;
import org.openrewrite.kotlin.KotlinTemplate;
import org.openrewrite.kotlin.KotlinVisitor;

import java.util.*;
import java.util.regex.Pattern;

import static java.util.Objects.requireNonNull;

/**
* Replaces deprecated Kotlin method calls based on {@code @Deprecated(replaceWith=ReplaceWith(...))} annotations.
* <p>
* This recipe takes a method pattern to match and a replacement expression that follows the Kotlin
* {@code ReplaceWith} annotation format.
*/
@Incubating(since = "8.43.0")
@EqualsAndHashCode(callSuper = false)
@Value
public class ReplaceDeprecatedKotlinMethod extends Recipe {

private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("\\b(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)\\b");
private static final Pattern TEMPLATE_PLACEHOLDER = Pattern.compile("#\\{([^}]+)}");

@Option(displayName = "Method pattern",
description = "A method pattern that is used to find matching method invocations.",
example = "arrow.core.MapKt mapOrAccumulate(kotlin.Function2)")
String methodPattern;

@Option(displayName = "Replacement",
description = "The replacement expression from `@Deprecated(replaceWith=ReplaceWith(...))`. " +
"Parameter names from the original method can be used directly.",
example = "mapValuesOrAccumulate(transform)")
String replacement;

@Option(displayName = "Imports",
description = "List of imports to add when the replacement is made.",
required = false,
example = "[\"arrow.core.Either\"]")
@Nullable
List<String> imports;

@Option(displayName = "Classpath from resources",
description = "List of classpath resource names for parsing the replacement template.",
required = false,
example = "[\"arrow-core-2\"]")
@Nullable
List<String> classpathFromResources;

String displayName = "Replace deprecated Kotlin method";
String description = "Replaces deprecated Kotlin method calls based on `@Deprecated(replaceWith=ReplaceWith(...))` annotations.";

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
MethodMatcher matcher = new MethodMatcher(methodPattern, true);
return Preconditions.check(new UsesMethod<>(methodPattern), new KotlinVisitor<ExecutionContext>() {

@Override
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation mi = (J.MethodInvocation) super.visitMethodInvocation(method, ctx);
if (matcher.matches(mi)) {
return replaceMethod(mi, ctx);
}
return mi;
}

private J replaceMethod(J.MethodInvocation method, ExecutionContext ctx) {
JavaType.Method methodType = method.getMethodType();
if (methodType == null) {
return method;
}

// Build the template string and extract parameters
TemplateConversion conversion = convertToTemplate(method, methodType);
if (conversion == null) {
return method;
}

// Add imports if specified
if (imports != null) {
for (String imp : imports) {
int lastDot = imp.lastIndexOf('.');
if (lastDot > 0) {
maybeAddImport(imp.substring(0, lastDot), imp.substring(lastDot + 1), false);
}
}
}

// Build and apply the template
KotlinTemplate.Builder templateBuilder = KotlinTemplate.builder(conversion.templateString);
if (imports != null) {
templateBuilder.imports(imports.toArray(new String[0]));
}
if (classpathFromResources != null && !classpathFromResources.isEmpty()) {
templateBuilder.parser(KotlinParser.builder()
.classpathFromResources(ctx, classpathFromResources.toArray(new String[0])));
}

J result = templateBuilder.build()
.apply(getCursor(), method.getCoordinates().replace(), conversion.parameters.toArray());

return result.withPrefix(method.getPrefix());
}

private TemplateConversion convertToTemplate(J.MethodInvocation method, JavaType.Method methodType) {
String templateString = replacement;
List<Object> parameters = new ArrayList<>();
Map<String, Expression> parameterLookup = new HashMap<>();

// Map 'this' to the select expression (receiver)
Expression select = method.getSelect();
if (select != null) {
parameterLookup.put("this", select);
}

// Map parameter names to their argument expressions
List<String> parameterNames = methodType.getParameterNames();
List<Expression> arguments = method.getArguments();
for (int i = 0; i < parameterNames.size() && i < arguments.size(); i++) {
parameterLookup.put(parameterNames.get(i), arguments.get(i));
}

// Also support positional references like p0, p1, etc.
for (int i = 0; i < arguments.size(); i++) {
parameterLookup.put("p" + i, arguments.get(i));
}

// Determine if this is an instance method call that needs a receiver
boolean needsReceiver = select != null && !replacement.startsWith("this.") &&
!replacement.contains(".") && !isStaticReplacement(replacement);

// Convert the replacement expression to a template
// Replace 'this.' prefix with receiver placeholder
if (templateString.startsWith("this.")) {
if (select != null) {
templateString = "#{any()}." + templateString.substring(5);
parameters.add(select);
} else {
// No select, just remove 'this.'
templateString = templateString.substring(5);
}
} else if (needsReceiver) {
// Prepend the receiver for instance method calls
templateString = "#{any()}." + templateString;
parameters.add(select);
}

// Now replace 'this' references that appear elsewhere
if (templateString.contains("this") && select != null) {
int thisCount = countOccurrences(templateString, "this");
templateString = templateString.replaceAll("\\bthis\\b", "#{any()}");
for (int i = 0; i < thisCount; i++) {
parameters.add(select);
}
}

// Find all identifiers in the template and replace with placeholders
Set<String> processedParams = new HashSet<>();
StringBuilder result = new StringBuilder();
java.util.regex.Matcher identifierMatcher = IDENTIFIER_PATTERN.matcher(templateString);
int lastEnd = 0;

while (identifierMatcher.find()) {
String identifier = identifierMatcher.group(1);

// Skip if already a placeholder or a keyword
if (identifier.equals("any") || identifier.equals("this") ||
Comment thread
timtebeek marked this conversation as resolved.
Outdated
Comment thread
timtebeek marked this conversation as resolved.
Outdated
Comment thread
timtebeek marked this conversation as resolved.
Outdated
isKotlinKeyword(identifier) || processedParams.contains(identifier)) {
continue;
}

Expression expr = parameterLookup.get(identifier);
if (expr != null && !processedParams.contains(identifier)) {
// This identifier is a parameter reference
result.append(templateString, lastEnd, identifierMatcher.start());
result.append("#{any()}");
parameters.add(expr);
processedParams.add(identifier);
lastEnd = identifierMatcher.end();
}
}
result.append(templateString.substring(lastEnd));
templateString = result.toString();

return new TemplateConversion(templateString, parameters);
}

private int countOccurrences(String str, String sub) {
int count = 0;
int idx = 0;
while ((idx = str.indexOf(sub, idx)) != -1) {
count++;
idx += sub.length();
}
return count;
}

private boolean isStaticReplacement(String replacement) {
// Check if the replacement looks like a static call or fully qualified reference
// e.g., "SomeClass.method()" or "somePackage.function()"
return replacement.contains(".") ||
(replacement.length() > 0 && Character.isUpperCase(replacement.charAt(0)));
}

private boolean isKotlinKeyword(String identifier) {
switch (identifier) {
case "as":
case "break":
case "class":
case "continue":
case "do":
case "else":
case "false":
case "for":
case "fun":
case "if":
case "in":
case "interface":
case "is":
case "null":
case "object":
case "package":
case "return":
case "super":
case "this":
case "throw":
case "true":
case "try":
case "typealias":
case "typeof":
case "val":
case "var":
case "when":
case "while":
return true;
default:
return false;
}
}
});
}

@Value
private static class TemplateConversion {
String templateString;
List<Object> parameters;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ ecosystem,packageName,name,displayName,description,recipeCount,category1,categor
maven,org.openrewrite:rewrite-kotlin,org.openrewrite.kotlin.FindKotlinSources,Find Kotlin sources and collect data metrics,Use data table to collect source files types and counts of files with extensions `.kt`.,1,,Kotlin,,Recipes to search and transform Kotlin.,"[{""name"":""markCompilationUnits"",""type"":""Boolean"",""displayName"":""Find Kotlin compilation units"",""description"":""Limit the search results to Kotlin CompilationUnits.""}]","[{""name"":""org.openrewrite.kotlin.table.KotlinSourceFile"",""displayName"":""Kotlin source files"",""description"":""Kotlin sources present in LSTs on the SAAS."",""columns"":[{""name"":""sourcePath"",""type"":""String"",""displayName"":""Source path before the run"",""description"":""The source path of the file before the run.""},{""name"":""sourceFileType"",""type"":""SourceFileType"",""displayName"":""Source file type"",""description"":""The source file type that was created.""}]}]"
maven,org.openrewrite:rewrite-kotlin,org.openrewrite.kotlin.OrderImports,Order Kotlin imports,"Groups and orders import statements. If a [style has been defined](https://docs.openrewrite.org/concepts-and-explanations/styles), this recipe will order the imports according to that style. If no style is detected, this recipe will default to ordering imports in the same way that IntelliJ IDEA does.",1,,Kotlin,,Recipes to search and transform Kotlin.,,
maven,org.openrewrite:rewrite-kotlin,org.openrewrite.kotlin.RenameTypeAlias,Rename type alias,Change the name of a given type alias.,1,,Kotlin,,Recipes to search and transform Kotlin.,"[{""name"":""aliasName"",""type"":""String"",""displayName"":""Old alias name"",""description"":""Name of the alias type."",""example"":""OldAlias"",""required"":true},{""name"":""newName"",""type"":""String"",""displayName"":""New alias name"",""description"":""Name of the alias type."",""example"":""NewAlias"",""required"":true},{""name"":""fullyQualifiedAliasedType"",""type"":""String"",""displayName"":""Target fully qualified type"",""description"":""Fully-qualified class name of the aliased type."",""example"":""org.junit.Assume"",""required"":true}]",
maven,org.openrewrite:rewrite-kotlin,org.openrewrite.kotlin.replace.ReplaceDeprecatedKotlinMethod,Replace deprecated Kotlin method,Replaces deprecated Kotlin method calls based on `@Deprecated(replaceWith=ReplaceWith(...))` annotations.,1,,Kotlin,,Recipes to search and transform Kotlin.,"[{""name"":""methodPattern"",""type"":""String"",""displayName"":""Method pattern"",""description"":""A method pattern that is used to find matching method invocations."",""example"":""arrow.core.MapKt mapOrAccumulate(kotlin.Function2)"",""required"":true},{""name"":""replacement"",""type"":""String"",""displayName"":""Replacement"",""description"":""The replacement expression from `@Deprecated(replaceWith=ReplaceWith(...))`. Parameter names from the original method can be used directly."",""example"":""mapValuesOrAccumulate(transform)"",""required"":true},{""name"":""imports"",""type"":""List"",""displayName"":""Imports"",""description"":""List of imports to add when the replacement is made."",""example"":""[\""arrow.core.Either\""]""},{""name"":""classpathFromResources"",""type"":""List"",""displayName"":""Classpath from resources"",""description"":""List of classpath resource names for parsing the replacement template."",""example"":""[\""arrow-core-2\""]""}]",
maven,org.openrewrite:rewrite-kotlin,org.openrewrite.kotlin.cleanup.EqualsMethodUsage,Structural equality tests should use `==` or `!=`,"In Kotlin, `==` means structural equality and `!=` structural inequality and both map to the left-side term’s `equals()` function. It is, therefore, redundant to call `equals()` as a function. Also, `==` and `!=` are more general than `equals()` and `!equals()` because it allows either of both operands to be `null`.
Developers using `equals()` instead of `==` or `!=` is often the result of adapting styles from other languages like Java, where `==` means reference equality and `!=` means reference inequality.
The `==` and `!=` operators are a more concise and elegant way to test structural equality than calling a function.",1,Cleanup,Kotlin,,Recipes to search and transform Kotlin.,,
Expand Down
Loading
Loading