diff --git a/src/main/java/org/openrewrite/kotlin/KotlinParser.java b/src/main/java/org/openrewrite/kotlin/KotlinParser.java index d1dd8782..50338f12 100644 --- a/src/main/java/org/openrewrite/kotlin/KotlinParser.java +++ b/src/main/java/org/openrewrite/kotlin/KotlinParser.java @@ -65,6 +65,7 @@ import org.jetbrains.kotlin.utils.PathUtil; import org.jspecify.annotations.Nullable; import org.openrewrite.*; +import org.openrewrite.internal.ListUtils; import org.openrewrite.java.JavaParser; import org.openrewrite.java.internal.JavaTypeCache; import org.openrewrite.java.marker.JavaSourceSet; @@ -87,7 +88,6 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import java.util.stream.Stream; import static java.util.Collections.emptyList; @@ -101,6 +101,7 @@ import static org.jetbrains.kotlin.config.JVMConfigurationKeys.DO_NOT_CLEAR_BINDING_CONTEXT; import static org.jetbrains.kotlin.config.JVMConfigurationKeys.LINK_VIA_SIGNATURES; import static org.jetbrains.kotlin.incremental.IncrementalFirJvmCompilerRunnerKt.configureBaseRoots; +import static org.openrewrite.kotlin.KotlinParser.SourcePathFromSourceTextResolver.determinePath; @SuppressWarnings("CommentedOutCode") @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @@ -115,6 +116,9 @@ public class KotlinParser implements Parser { @Nullable private final Collection classpath; + @Nullable + private final List dependsOn; + private final List styles; private final boolean logCompilationWarningsAndErrors; private final JavaTypeCache typeCache; @@ -162,10 +166,11 @@ public Stream parseInputs(Iterable sources, @Nullable Path re // TODO: FIR and disposable may not be necessary using the IR. Disposable disposable = Disposer.newDisposable(); CompiledSource compilerCus; + List acceptedInputs = ListUtils.concatAll(dependsOn, acceptedInputs(sources).collect(toList())); try { - compilerCus = parse(acceptedInputs(sources).collect(Collectors.toList()), disposable, pctx); + compilerCus = parse(acceptedInputs, disposable, pctx); } catch (Exception e) { - return acceptedInputs(sources).map(input -> ParseError.build(this, input, relativeTo, ctx, e)); + return acceptedInputs.stream().map(input -> ParseError.build(this, input, relativeTo, ctx, e)); } FirSession firSession = compilerCus.getFirSession(); @@ -201,7 +206,8 @@ public Stream parseInputs(Iterable sources, @Nullable Path re return (SourceFile) null; }) .limit(1)) - .filter(Objects::nonNull); + .filter(Objects::nonNull) + .filter(source -> !source.getSourcePath().getFileName().toString().startsWith("dependsOn-")); } @Override @@ -252,6 +258,7 @@ public static class Builder extends Parser.Builder { @Nullable private Collection classpath = emptyList(); + private List dependsOn = emptyList(); private JavaTypeCache typeCache = new JavaTypeCache(); private boolean logCompilationWarningsAndErrors; private final List styles = new ArrayList<>(); @@ -303,6 +310,13 @@ public Builder addClasspathEntry(Path classpath) { return this; } + public Builder dependsOn(@Language("kotlin") String... inputsAsStrings) { + this.dependsOn = Arrays.stream(inputsAsStrings) + .map(input -> Input.fromString(determinePath("dependsOn-", input), input)) + .collect(toList()); + return this; + } + public Builder typeCache(JavaTypeCache typeCache) { this.typeCache = typeCache; return this; @@ -335,7 +349,7 @@ public Builder languageLevel(KotlinLanguageLevel languageLevel) { @Override public KotlinParser build() { - return new KotlinParser(resolvedClasspath(), styles, logCompilationWarningsAndErrors, typeCache, moduleName, languageLevel, isKotlinScript); + return new KotlinParser(resolvedClasspath(), dependsOn, styles, logCompilationWarningsAndErrors, typeCache, moduleName, languageLevel, isKotlinScript); } @Override @@ -609,4 +623,27 @@ private List getCRLFLocations(String source) { return cRLFIndices; } + + static class SourcePathFromSourceTextResolver { + private static final Pattern packagePattern = Pattern.compile("^package\\s+(\\S+)"); + private static final Pattern classPattern = Pattern.compile("(class|interface|enum class)\\s*(<[^>]*>)?\\s+(\\w+)"); + private static final Pattern publicClassPattern = Pattern.compile("public\\s+" + classPattern.pattern()); + + private static Optional matchClassPattern(Pattern pattern, String source) { + Matcher classMatcher = pattern.matcher(source); + if (classMatcher.find()) { + return Optional.of(classMatcher.group(3)); + } + return Optional.empty(); + } + + static Path determinePath(String prefix, String sourceCode) { + String className = matchClassPattern(publicClassPattern, sourceCode) + .orElseGet(() -> matchClassPattern(classPattern, sourceCode) + .orElse(Long.toString(System.nanoTime()))); + Matcher packageMatcher = packagePattern.matcher(sourceCode); + String pkg = packageMatcher.find() ? packageMatcher.group(1).replace('.', '/') + "/" : ""; + return Paths.get(pkg, prefix + className + ".kt"); + } + } } diff --git a/src/test/java/org/openrewrite/kotlin/KotlinParserTest.java b/src/test/java/org/openrewrite/kotlin/KotlinParserTest.java new file mode 100644 index 00000000..1b1b1d74 --- /dev/null +++ b/src/test/java/org/openrewrite/kotlin/KotlinParserTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * 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 + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * 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; + +import org.junit.jupiter.api.Test; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.kotlin.Assertions.kotlin; + +class KotlinParserTest implements RewriteTest { + + @Test + void classDefinitionFromDependsOn() { + rewriteRun( + spec -> spec.parser(KotlinParser.builder().dependsOn(""" + package foo.bar + + class MyClass + """)), + kotlin( + """ + import foo.bar.MyClass + + val myClass: MyClass? = null + """ + ) + ); + } + +}