diff --git a/src/main/java/org/openrewrite/kotlin/Assertions.java b/src/main/java/org/openrewrite/kotlin/Assertions.java index bcfd05c5f..0d2934b5f 100644 --- a/src/main/java/org/openrewrite/kotlin/Assertions.java +++ b/src/main/java/org/openrewrite/kotlin/Assertions.java @@ -21,24 +21,30 @@ import org.openrewrite.*; import org.openrewrite.internal.ThrowingConsumer; import org.openrewrite.internal.lang.Nullable; -import org.openrewrite.java.tree.J; -import org.openrewrite.java.tree.Space; -import org.openrewrite.java.tree.TextComment; +import org.openrewrite.java.search.FindMissingTypes; +import org.openrewrite.java.tree.*; +import org.openrewrite.kotlin.marker.AnnotationUseSite; +import org.openrewrite.kotlin.marker.Extension; +import org.openrewrite.kotlin.marker.IndexedAccess; import org.openrewrite.kotlin.tree.K; import org.openrewrite.kotlin.tree.KSpace; +import org.openrewrite.marker.Marker; import org.openrewrite.marker.Markers; +import org.openrewrite.marker.SearchResult; import org.openrewrite.test.SourceSpec; import org.openrewrite.test.SourceSpecs; +import org.openrewrite.test.TypeValidation; import org.openrewrite.test.UncheckedConsumer; import org.opentest4j.AssertionFailedError; -import java.util.Collections; -import java.util.Optional; +import java.util.*; import java.util.function.Consumer; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; import static org.openrewrite.java.Assertions.sourceSet; +import static org.openrewrite.java.tree.TypeUtils.isWellFormedType; import static org.openrewrite.test.SourceSpecs.dir; @SuppressWarnings({"unused", "unchecked", "OptionalGetWithoutIsPresent", "DataFlowIssue"}) @@ -47,6 +53,13 @@ public final class Assertions { private Assertions() { } + public static SourceFile validateTypes(SourceFile source, TypeValidation typeValidation) { + if (source instanceof JavaSourceFile) { + assertValidTypes(typeValidation, (JavaSourceFile) source); + } + return source; + } + static void customizeExecutionContext(ExecutionContext ctx) { if (ctx.getMessage(KotlinParser.SKIP_SOURCE_SET_TYPE_GENERATION) == null) { ctx.putMessage(KotlinParser.SKIP_SOURCE_SET_TYPE_GENERATION, true); @@ -110,7 +123,7 @@ public static SourceSpecs kotlinScript(@Language("kts") @Nullable String before) public static SourceSpecs kotlin(@Language("kotlin") @Nullable String before, Consumer> spec) { SourceSpec kotlin = new SourceSpec<>( K.CompilationUnit.class, null, KotlinParser.builder(), before, - SourceSpec.ValidateSource.noop, + Assertions::validateTypes, Assertions::customizeExecutionContext ); acceptSpec(spec, kotlin); @@ -120,7 +133,7 @@ public static SourceSpecs kotlin(@Language("kotlin") @Nullable String before, Co public static SourceSpecs kotlinScript(@Language("kts") @Nullable String before, Consumer> spec) { SourceSpec kotlinScript = new SourceSpec<>( K.CompilationUnit.class, null, KotlinParser.builder().isKotlinScript(true), before, - SourceSpec.ValidateSource.noop, + Assertions::validateTypes, Assertions::customizeExecutionContext ); acceptSpec(spec, kotlinScript); @@ -135,7 +148,7 @@ public static SourceSpecs kotlin(@Language("kotlin") @Nullable String before, @L public static SourceSpecs kotlin(@Language("kotlin") @Nullable String before, @Language("kotlin") String after, Consumer> spec) { SourceSpec kotlin = new SourceSpec<>(K.CompilationUnit.class, null, KotlinParser.builder(), before, - SourceSpec.ValidateSource.noop, + Assertions::validateTypes, Assertions::customizeExecutionContext).after(s -> after); acceptSpec(spec, kotlin); return kotlin; @@ -196,6 +209,36 @@ public static UncheckedConsumer> spaceConscious() { }; } + private static void assertValidTypes(TypeValidation typeValidation, J sf) { + if (typeValidation.identifiers() || typeValidation.methodInvocations() || typeValidation.methodDeclarations() || typeValidation.classDeclarations() + || typeValidation.constructorInvocations()) { + List missingTypeResults = findMissingTypes(sf); + missingTypeResults = missingTypeResults.stream() + .filter(missingType -> { + if (missingType.getJ() instanceof J.Identifier) { + return typeValidation.identifiers(); + } else if (missingType.getJ() instanceof J.ClassDeclaration) { + return typeValidation.classDeclarations(); + } else if (missingType.getJ() instanceof J.MethodInvocation || missingType.getJ() instanceof J.MemberReference) { + return typeValidation.methodInvocations(); + } else if (missingType.getJ() instanceof J.NewClass) { + return typeValidation.constructorInvocations(); + } else if (missingType.getJ() instanceof J.MethodDeclaration) { + return typeValidation.methodDeclarations(); + } else if (missingType.getJ() instanceof J.VariableDeclarations.NamedVariable) { + return typeValidation.variableDeclarations(); + } else { + return true; + } + }) + .collect(Collectors.toList()); + if (!missingTypeResults.isEmpty()) { + throw new IllegalStateException("LST contains missing or invalid type information\n" + missingTypeResults.stream().map(v -> v.getPath() + "\n" + v.getPrintedTree()) + .collect(Collectors.joining("\n\n"))); + } + } + } + public static ThrowingConsumer spaceConscious(SourceSpec spec) { return cu -> { K.CompilationUnit visited = (K.CompilationUnit) new KotlinIsoVisitor() { @@ -247,4 +290,287 @@ public Space visitSpace(Space space, Space.Location loc, Integer integer) { } }; } + + public static List findMissingTypes(J j) { + J j1 = new FindMissingTypesVisitor().visit(j, new InMemoryExecutionContext()); + List results = new ArrayList<>(); + if (j1 != j) { + new KotlinIsoVisitor>() { + @Override + public M visitMarker(Marker marker, List missingTypeResults) { + if (marker instanceof SearchResult) { + String message = ((SearchResult) marker).getDescription(); + String path = getCursor() + .getPathAsStream(j -> j instanceof J || j instanceof Javadoc) + .map(t -> t.getClass().getSimpleName()) + .collect(Collectors.joining("->")); + J j = getCursor().firstEnclosing(J.class); + String printedTree; + if (getCursor().firstEnclosing(JavaSourceFile.class) != null) { + printedTree = j != null ? j.printTrimmed(new InMemoryExecutionContext(), getCursor().getParentOrThrow()) : ""; + } else { + printedTree = String.valueOf(j); + } + missingTypeResults.add(new FindMissingTypes.MissingTypeResult(message, path, printedTree, j)); + } + return super.visitMarker(marker, missingTypeResults); + } + }.visit(j1, results); + } + return results; + } + + static class FindMissingTypesVisitor extends KotlinIsoVisitor { + + private final Set seenTypes = new HashSet<>(); + + @Override + public J.Identifier visitIdentifier(J.Identifier identifier, ExecutionContext ctx) { + // The non-nullability of J.Identifier.getType() in our AST is a white lie + // J.Identifier.getType() is allowed to be null in places where the containing AST element fully specifies the type + if (!isWellFormedType(identifier.getType(), seenTypes) && !isAllowedToHaveNullType(identifier)) { + if (isValidated(identifier)) { + identifier = SearchResult.found(identifier, "Identifier type is missing or malformed"); + } + } + if (identifier.getFieldType() != null && !identifier.getSimpleName().equals(identifier.getFieldType().getName()) && isNotDestructType(identifier.getFieldType())) { + identifier = SearchResult.found(identifier, "type information has a different variable name '" + identifier.getFieldType().getName() + "'"); + } + return identifier; + } + + @Override + public J.VariableDeclarations.NamedVariable visitVariable(J.VariableDeclarations.NamedVariable variable, ExecutionContext ctx) { + J.VariableDeclarations.NamedVariable v = super.visitVariable(variable, ctx); + if (v == variable) { + JavaType.Variable variableType = v.getVariableType(); + if (!isWellFormedType(variableType, seenTypes) && !isAllowedToHaveUnknownType()) { + if (isValidated(variable)) { + v = SearchResult.found(v, "Variable type is missing or malformed"); + } + } else if (variableType != null && !variableType.getName().equals(v.getSimpleName()) && isNotDestructType(variableType)) { + v = SearchResult.found(v, "type information has a different variable name '" + variableType.getName() + "'"); + } + } + return v; + } + + private boolean isAllowedToHaveUnknownType() { + Cursor parent = getCursor().getParent(); + while (parent != null && parent.getParent() != null && !(parent.getParentTreeCursor().getValue() instanceof J.ClassDeclaration)) { + parent = parent.getParentTreeCursor(); + } + // If the variable is declared in a class initializer, then it's allowed to have unknown type + return parent != null && parent.getValue() instanceof J.Block; + } + + @Override + public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = super.visitMethodInvocation(method, ctx); + // If one of the method's arguments or type parameters is missing type, then the invocation very likely will too + // Avoid over-reporting the same problem by checking the invocation only when its elements are well-formed + if (mi == method) { + JavaType.Method type = mi.getMethodType(); + if (!isWellFormedType(type, seenTypes)) { + mi = SearchResult.found(mi, "MethodInvocation type is missing or malformed"); + } else if (!type.getName().equals(mi.getSimpleName()) && !type.isConstructor() && isValidated(mi)) { + mi = SearchResult.found(mi, "type information has a different method name '" + type.getName() + "'"); + } + if (mi.getName().getType() != null && type != mi.getName().getType()) { + mi = SearchResult.found(mi, "MethodInvocation#name type is not the MethodType of MethodInvocation."); + } + } + return mi; + } + + @Override + public J.MemberReference visitMemberReference(J.MemberReference memberRef, ExecutionContext ctx) { + J.MemberReference mr = super.visitMemberReference(memberRef, ctx); + JavaType.Method type = mr.getMethodType(); + if (type != null) { + if (!isWellFormedType(type, seenTypes)) { + mr = SearchResult.found(mr, "MemberReference type is missing or malformed"); + } else if (!type.getName().equals(mr.getReference().getSimpleName()) && !type.isConstructor()) { + mr = SearchResult.found(mr, "type information has a different method name '" + type.getName() + "'"); + } + } else { + JavaType.Variable variableType = mr.getVariableType(); + if (!isWellFormedType(variableType, seenTypes)) { + if (!"class".equals(mr.getReference().getSimpleName())) { + mr = SearchResult.found(mr, "MemberReference type is missing or malformed"); + } + } else if (!variableType.getName().equals(mr.getReference().getSimpleName())) { + mr = SearchResult.found(mr, "type information has a different variable name '" + variableType.getName() + "'"); + } + } + return mr; + } + + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx); + JavaType.Method type = md.getMethodType(); + if (!isWellFormedType(type, seenTypes)) { + md = SearchResult.found(md, "MethodDeclaration type is missing or malformed"); + } else if (!md.getSimpleName().equals(type.getName()) && !type.isConstructor() && !"anonymous".equals(type.getName())) { + md = SearchResult.found(md, "type information has a different method name '" + type.getName() + "'"); + } + if (md.getName().getType() != null && type != md.getName().getType()) { + md = SearchResult.found(md, "MethodDeclaration#name type is not the MethodType of MethodDeclaration."); + } + return md; + } + + @Override + public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) { + J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx); + JavaType.FullyQualified t = cd.getType(); + if (!isWellFormedType(t, seenTypes)) { + return SearchResult.found(cd, "ClassDeclaration type is missing or malformed"); + } + if (!cd.getKind().name().equals(t.getKind().name())) { + cd = SearchResult.found(cd, + " J.ClassDeclaration kind " + cd.getKind() + " does not match the kind in its type information " + t.getKind()); + } + J.CompilationUnit jc = getCursor().firstEnclosing(J.CompilationUnit.class); + if (jc != null) { + J.Package pkg = jc.getPackageDeclaration(); + if (pkg != null && t.getPackageName().equals(pkg.printTrimmed(getCursor()))) { + cd = SearchResult.found(cd, + " J.ClassDeclaration package " + pkg + " does not match the package in its type information " + pkg.printTrimmed(getCursor())); + } + } + return cd; + } + + @Override + public J.NewClass visitNewClass(J.NewClass newClass, ExecutionContext ctx) { + J.NewClass n = super.visitNewClass(newClass, ctx); + if (n == newClass && !isWellFormedType(n.getType(), seenTypes)) { + n = SearchResult.found(n, "NewClass type is missing or malformed"); + } + if (n.getClazz() instanceof J.Identifier && n.getClazz().getType() != null && + !(n.getClazz().getType() instanceof JavaType.Class || n.getClazz().getType() instanceof JavaType.Unknown)) { + n = SearchResult.found(n, "NewClass#clazz is J.Identifier and the type is is not JavaType$Class."); + } + return n; + } + + @Override + public J.ParameterizedType visitParameterizedType(J.ParameterizedType type, ExecutionContext ctx) { + J.ParameterizedType p = super.visitParameterizedType(type, ctx); + if (p.getClazz() instanceof J.Identifier && p.getClazz().getType() != null && + !(p.getClazz().getType() instanceof JavaType.Class || p.getClazz().getType() instanceof JavaType.Unknown)) { + p = SearchResult.found(p, "ParameterizedType#clazz is J.Identifier and the type is is not JavaType$Class."); + } + return p; + } + + private boolean isAllowedToHaveNullType(J.Identifier ident) { + return inPackageDeclaration() || inImport() || isClassName() + || isMethodName() || isMethodInvocationName() || isFieldAccess(ident) || isBeingDeclared(ident) || isParameterizedType(ident) + || isNewClass(ident) || isTypeParameter() || isMemberReference(ident) || isCaseLabel() || isLabel() || isAnnotationField(ident) + || isInJavaDoc(ident) || isWhenLabel(); + } + + private boolean inPackageDeclaration() { + return getCursor().firstEnclosing(J.Package.class) != null; + } + + private boolean inImport() { + return getCursor().firstEnclosing(J.Import.class) != null; + } + + private boolean isClassName() { + Cursor parent = getCursor().getParent(); + return parent != null && parent.getValue() instanceof J.ClassDeclaration; + } + + private boolean isMethodName() { + Cursor parent = getCursor().getParent(); + return parent != null && parent.getValue() instanceof J.MethodDeclaration; + } + + private boolean isMethodInvocationName() { + Cursor parent = getCursor().getParent(); + return parent != null && parent.getValue() instanceof J.MethodInvocation; + } + + private boolean isFieldAccess(J.Identifier ident) { + Tree value = getCursor().getParentTreeCursor().getValue(); + return value instanceof J.FieldAccess + && (ident == ((J.FieldAccess) value).getName() || + ident == ((J.FieldAccess) value).getTarget() && !((J.FieldAccess) value).getSimpleName().equals("class")); + } + + private boolean isBeingDeclared(J.Identifier ident) { + Tree value = getCursor().getParentTreeCursor().getValue(); + return value instanceof J.VariableDeclarations.NamedVariable && ident == ((J.VariableDeclarations.NamedVariable) value).getName(); + } + + private boolean isParameterizedType(J.Identifier ident) { + Tree value = getCursor().getParentTreeCursor().getValue(); + return value instanceof J.ParameterizedType && ident == ((J.ParameterizedType) value).getClazz(); + } + + private boolean isNewClass(J.Identifier ident) { + Tree value = getCursor().getParentTreeCursor().getValue(); + return value instanceof J.NewClass && ident == ((J.NewClass) value).getClazz(); + } + + private boolean isTypeParameter() { + return getCursor().getParent() != null + && getCursor().getParent().getValue() instanceof J.TypeParameter; + } + + private boolean isMemberReference(J.Identifier ident) { + Tree value = getCursor().getParentTreeCursor().getValue(); + return value instanceof J.MemberReference && + ident == ((J.MemberReference) value).getReference(); + } + + private boolean isInJavaDoc(J.Identifier ident) { + Tree value = getCursor().getParentTreeCursor().getValue(); + return value instanceof Javadoc.Reference && + ident == ((Javadoc.Reference) value).getTree(); + } + + private boolean isCaseLabel() { + return getCursor().getParentTreeCursor().getValue() instanceof J.Case; + } + + + private boolean isWhenLabel() { + return getCursor().getParentTreeCursor().getValue() instanceof K.WhenBranch; + } + + private boolean isLabel() { + return getCursor().firstEnclosing(J.Label.class) != null; + } + + private boolean isAnnotationField(J.Identifier ident) { + Cursor parent = getCursor().getParent(); + return parent != null && parent.getValue() instanceof J.Assignment + && (ident == ((J.Assignment) parent.getValue()).getVariable() && getCursor().firstEnclosing(J.Annotation.class) != null); + } + + private boolean isValidated(J.Identifier i) { + J j = getCursor().dropParentUntil(it -> it instanceof J).getValue(); + // TODO: replace with AnnotationUseSite tree. + return !j.getMarkers().findFirst(AnnotationUseSite.class).isPresent() && !(j instanceof K.KReturn); + } + + private boolean isValidated(J.MethodInvocation mi) { + return !mi.getMarkers().findFirst(IndexedAccess.class).isPresent(); + } + + private boolean isValidated(J.VariableDeclarations.NamedVariable v) { + J.VariableDeclarations j = getCursor().firstEnclosing(J.VariableDeclarations.class); + return j.getModifiers().stream().noneMatch(it -> "typealias".equals(it.getKeyword())) && !j.getMarkers().findFirst(Extension.class).isPresent(); + } + + private boolean isNotDestructType(JavaType.Variable variable) { + return !"".equals(variable.getName()); + } + } } diff --git a/src/main/java/org/openrewrite/kotlin/internal/KotlinPrinter.java b/src/main/java/org/openrewrite/kotlin/internal/KotlinPrinter.java index 91e5690d4..a16b7dd18 100755 --- a/src/main/java/org/openrewrite/kotlin/internal/KotlinPrinter.java +++ b/src/main/java/org/openrewrite/kotlin/internal/KotlinPrinter.java @@ -1162,7 +1162,7 @@ public J visitVariableDeclarations(J.VariableDeclarations multiVariable, PrintOu p.append("val"); } - if (m.getType() == J.Modifier.Type.LanguageExtension && m.getKeyword().equals("typealias")) { + if (m.getType() == J.Modifier.Type.LanguageExtension && m.getKeyword() != null && m.getKeyword().equals("typealias")) { isTypeAlias = true; } } diff --git a/src/main/java/org/openrewrite/kotlin/search/package-info.java b/src/main/java/org/openrewrite/kotlin/search/package-info.java new file mode 100644 index 000000000..d1c0102b9 --- /dev/null +++ b/src/main/java/org/openrewrite/kotlin/search/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ +@NonNullApi +@NonNullFields +package org.openrewrite.kotlin.search; + +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.internal.lang.NonNullFields; diff --git a/src/test/java/org/openrewrite/kotlin/KotlinTypeMappingTest.java b/src/test/java/org/openrewrite/kotlin/KotlinTypeMappingTest.java index 8935ec4da..bf9dfb4f1 100644 --- a/src/test/java/org/openrewrite/kotlin/KotlinTypeMappingTest.java +++ b/src/test/java/org/openrewrite/kotlin/KotlinTypeMappingTest.java @@ -31,6 +31,7 @@ import org.openrewrite.java.tree.TypeUtils; import org.openrewrite.kotlin.tree.K; import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; @@ -947,6 +948,7 @@ class A : RemoteStub() @Test void unknownIdentifier() { rewriteRun( + spec -> spec.typeValidationOptions(TypeValidation.none()), kotlin( //language=none "class A : RemoteStub", diff --git a/src/test/java/org/openrewrite/kotlin/RemoveImportTest.java b/src/test/java/org/openrewrite/kotlin/RemoveImportTest.java index a0bd0e600..8c2ef47e1 100644 --- a/src/test/java/org/openrewrite/kotlin/RemoveImportTest.java +++ b/src/test/java/org/openrewrite/kotlin/RemoveImportTest.java @@ -20,6 +20,7 @@ import org.openrewrite.Recipe; import org.openrewrite.kotlin.tree.K; import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; import static org.openrewrite.kotlin.Assertions.kotlin; import static org.openrewrite.test.RewriteTest.toRecipe; @@ -34,9 +35,9 @@ void jvmStaticMember() { """ import java.lang.Integer import java.lang.Long - + import java.lang.Integer.MAX_VALUE - + class A """, """ @@ -56,7 +57,7 @@ void removeStarFoldPackage() { kotlin( """ import java.io.* - + class A { val f = File("foo") } @@ -79,7 +80,7 @@ void keepStarFoldPackage() { kotlin( """ import java.io.* - + class A { val c : Closeable? = null val f : File? = null @@ -99,7 +100,7 @@ void removeStarFoldTypeMembers() { kotlin( """ import java.util.regex.Pattern.* - + class A { val i = CASE_INSENSITIVE val x = COMMENTS @@ -108,7 +109,7 @@ class A { """ import java.util.regex.Pattern.CASE_INSENSITIVE import java.util.regex.Pattern.COMMENTS - + class A { val i = CASE_INSENSITIVE val x = COMMENTS @@ -125,7 +126,7 @@ void keepStarFoldTypeMembers() { kotlin( """ import java.util.regex.Pattern.* - + class A { val i = CASE_INSENSITIVE val x = COMMENTS @@ -139,12 +140,13 @@ class A { @Test void keepImportAlias() { rewriteRun( - spec -> spec.recipe(removeMemberImportRecipe("java.util.regex.Pattern", "COMMENTS")), + spec -> spec.typeValidationOptions(TypeValidation.none()) + .recipe(removeMemberImportRecipe("java.util.regex.Pattern", "COMMENTS")), kotlin( """ import java.util.regex.Pattern.CASE_INSENSITIVE as i import java.util.regex.Pattern.COMMENTS as x - + class A { val f = arrayOf(i, x) } @@ -157,19 +159,21 @@ class A { void removeImportAlias() { // TODO check if this is really what we want to happen rewriteRun( - spec -> spec.recipe(removeMemberImportRecipe("java.util.regex.Pattern", "COMMENTS")), + // Type validation is disabled until https://github.com/openrewrite/rewrite-kotlin/issues/545 is implemented. + spec -> spec.typeValidationOptions(TypeValidation.none()) + .recipe(removeMemberImportRecipe("java.util.regex.Pattern", "COMMENTS")), kotlin( """ import java.util.regex.Pattern.CASE_INSENSITIVE as i import java.util.regex.Pattern.COMMENTS as x - + class A { val f = arrayOf(i) } """, """ import java.util.regex.Pattern.CASE_INSENSITIVE as i - + class A { val f = arrayOf(i) } diff --git a/src/test/java/org/openrewrite/kotlin/format/TabsAndIndentsTest.java b/src/test/java/org/openrewrite/kotlin/format/TabsAndIndentsTest.java index 36ca49881..d8aa75212 100644 --- a/src/test/java/org/openrewrite/kotlin/format/TabsAndIndentsTest.java +++ b/src/test/java/org/openrewrite/kotlin/format/TabsAndIndentsTest.java @@ -28,6 +28,7 @@ import org.openrewrite.style.NamedStyles; import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; import java.util.function.Consumer; import java.util.function.UnaryOperator; @@ -2223,8 +2224,11 @@ fun test0() { @Test void trailingLambdaCall() { rewriteRun( + spec -> spec.typeValidationOptions(TypeValidation.none()), kotlin( """ + import kotlin.reflect.full.memberProperties + inline fun T.destruct(): Map { return T::class.memberProperties.map { it.name to it.get(this) diff --git a/src/test/java/org/openrewrite/kotlin/tree/AnnotationTest.java b/src/test/java/org/openrewrite/kotlin/tree/AnnotationTest.java index 867b42ba4..82f8c3c08 100644 --- a/src/test/java/org/openrewrite/kotlin/tree/AnnotationTest.java +++ b/src/test/java/org/openrewrite/kotlin/tree/AnnotationTest.java @@ -15,8 +15,6 @@ */ package org.openrewrite.kotlin.tree; -import org.intellij.lang.annotations.Language; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -24,39 +22,16 @@ import org.openrewrite.java.tree.J; import org.openrewrite.kotlin.KotlinParser; import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.openrewrite.kotlin.Assertions.kotlin; -@SuppressWarnings({"RedundantSuppression", "RedundantNullableReturnType", "RedundantVisibilityModifier", "UnusedReceiverParameter", "SortModifiers", "TrailingComma"}) +@SuppressWarnings({"RedundantSuppression", "RedundantNullableReturnType", "RedundantVisibilityModifier", "UnusedReceiverParameter", "SortModifiers", "TrailingComma", "RedundantGetter", "RedundantSetter"}) class AnnotationTest implements RewriteTest { - @Language("kotlin") - private static final String ANNOTATION = - """ - @Target( - AnnotationTarget.CLASS, - AnnotationTarget.ANNOTATION_CLASS, - AnnotationTarget.TYPE_PARAMETER, - AnnotationTarget.PROPERTY, - AnnotationTarget.FIELD, - AnnotationTarget.LOCAL_VARIABLE, - AnnotationTarget.VALUE_PARAMETER, - AnnotationTarget.CONSTRUCTOR, - AnnotationTarget.FUNCTION, - AnnotationTarget.PROPERTY_GETTER, - AnnotationTarget.PROPERTY_SETTER, - AnnotationTarget.TYPE, - AnnotationTarget.EXPRESSION, - AnnotationTarget.FILE, - AnnotationTarget.TYPEALIAS - ) - @Retention(AnnotationRetention.SOURCE) - annotation class Ann - """; - @Test void fileScope() { rewriteRun( @@ -74,12 +49,18 @@ class A @Test void multipleFileScope() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ - @file : Ann - @file : Ann - @file : Ann + @Target(AnnotationTarget.FILE) + @Retention(AnnotationRetention.SOURCE) + annotation class Anno + """), + kotlin( + //language=none + """ + @file : Anno + @file : Anno + @file : Anno """ ) ); @@ -89,11 +70,11 @@ void multipleFileScope() { void annotationOnEnumEntry() { rewriteRun( spec -> spec.parser(KotlinParser.builder().classpath()), - kotlin(ANNOTATION), kotlin( """ + annotation class Anno enum class EnumTypeA { - @Ann + @Anno FOO } """ @@ -139,14 +120,11 @@ void arrayArgument() { rewriteRun( kotlin( """ - @Target ( AnnotationTarget . LOCAL_VARIABLE ) + @Target ( AnnotationTarget . PROPERTY ) @Retention ( AnnotationRetention . SOURCE ) - annotation class Test ( val values : Array < String > ) - """ - ), - kotlin( - """ - @Test( values = [ "a" , "b" , "c" ] ) + annotation class Anno ( val values : Array < String > ) + + @Anno( values = [ "a" , "b" , "c" ] ) val a = 42 """ ) @@ -155,10 +133,11 @@ annotation class Test ( val values : Array < String > ) @Test void fullyQualifiedAnnotation() { + //noinspection RemoveRedundantQualifierName rewriteRun( kotlin( """ - @java.lang.Deprecated + @kotlin.Deprecated("") class A """ ) @@ -170,8 +149,8 @@ void trailingComma() { rewriteRun( kotlin( """ - annotation class Test ( val values : Array < String > ) - @Test( values = [ "a" , "b" , /* trailing comma */ ] ) + annotation class Anno ( val values : Array < String > ) + @Anno( values = [ "a" , "b" , /* trailing comma */ ] ) val a = 42 """ ) @@ -197,12 +176,12 @@ void jvmNameAnnotation() { @Test void annotationUseSiteTargetAnnotationOnly() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ + annotation class Anno class TestA { - @get : Ann - @set : Ann + @get : Anno + @set : Anno var name : String = "" } """ @@ -214,23 +193,24 @@ class TestA { @Test void annotationUseSiteTarget() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ + annotation class Anno class TestA { - @get : Ann + @get : Anno public - @set : Ann + @set : Anno var name: String = "" } """ ), kotlin( """ + annotation class Anno class TestB { public - @get : Ann - @set : Ann + @get : Anno + @set : Anno var name : String = "" } """ @@ -242,11 +222,11 @@ class TestB { @Test void constructorParameterWithAnnotation() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ + annotation class Anno class Example( - @get : Ann + @get : Anno val bar : String ) """ @@ -258,10 +238,10 @@ class Example( @Test void getUseSiteOnConstructorParams() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ - class Example ( /**/ /**/ @get : Ann /**/ /**/ @set : Ann /**/ /**/ var foo: String , @get : Ann val bar: String ) + annotation class Anno + class Example ( /**/ /**/ @get : Anno /**/ /**/ @set : Anno /**/ /**/ var foo: String , @get : Anno val bar: String ) """ ) ); @@ -270,16 +250,16 @@ class Example ( /**/ /**/ @get : Ann /**/ /**/ @set : Ann /**/ /**/ var foo: St @Test void annotationOnExplicitGetter() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ + annotation class Anno class Test { public var stringRepresentation : String = "" - @Ann + @Anno // comment get ( ) = field - @Ann + @Anno set ( value ) { field = value } @@ -292,10 +272,10 @@ class Test { @Test void paramAnnotation() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ - class Example ( @param : Ann val quux : String ) + annotation class Anno + class Example ( @param : Anno val quux : String ) """ ) ); @@ -304,10 +284,10 @@ class Example ( @param : Ann val quux : String ) @Test void fieldAnnotation() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ - class Example ( @field : Ann val foo : String ) + annotation class Anno + class Example ( @field : Anno val foo : String ) """ ) ); @@ -316,10 +296,10 @@ class Example ( @field : Ann val foo : String ) @Test void receiverAnnotationUseSiteTarget() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ - fun @receiver : Ann String . myExtension ( ) { } + annotation class Anno + fun @receiver : Anno String . myExtension ( ) { } """ ) ); @@ -328,11 +308,13 @@ void receiverAnnotationUseSiteTarget() { @Test void setParamAnnotationUseSiteTarget() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ + annotation class Anno + class Example { - @setparam : Ann + @setparam : Anno + @set: Anno var name: String = "" } """ @@ -343,11 +325,13 @@ class Example { @Test void destructuringVariableDeclaration() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ + @file:Suppress("UNUSED_VARIABLE") + annotation class Anno + fun example ( ) { - val ( @Ann a , @Ann b , @Ann c ) = Triple ( 1 , 2 , 3 ) + val ( @Anno a , @Anno b , @Anno c ) = Triple ( 1 , 2 , 3 ) } """ ) @@ -357,9 +341,29 @@ fun example ( ) { @Test void annotationsInManyLocations() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ + @Target( + AnnotationTarget.CLASS, + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.TYPE_PARAMETER, + AnnotationTarget.PROPERTY, + AnnotationTarget.FIELD, + AnnotationTarget.LOCAL_VARIABLE, + AnnotationTarget.VALUE_PARAMETER, + AnnotationTarget.CONSTRUCTOR, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.TYPE, + AnnotationTarget.EXPRESSION, + AnnotationTarget.FILE, + AnnotationTarget.TYPEALIAS + ) + @Retention(AnnotationRetention.SOURCE) + annotation class Ann + + @Suppress("RedundantSetter","RedundantSuppression") @Ann open class Test < @Ann in Number > ( @Ann val s : String ) { @Ann var n : Int = 42 @@ -388,7 +392,7 @@ annotation class B annotation class C annotation class LAST - @A final @B internal @C @LAST class Foo + @A open @B internal @C @LAST class Foo """, spec -> spec.afterRecipe(cu -> { J.ClassDeclaration last = (J.ClassDeclaration) cu.getStatements().get(cu.getStatements().size() - 1); @@ -404,12 +408,15 @@ annotation class LAST @Test void lambdaExpression() { rewriteRun( - kotlin(ANNOTATION), kotlin( """ + @Target(AnnotationTarget.EXPRESSION) + @Retention(AnnotationRetention.SOURCE) + annotation class Anno + fun method ( ) { val list = listOf ( 1 , 2 , 3 ) - list . filterIndexed { index , _ -> @Ann index % 2 == 0 } + list . filterIndexed { index , _ -> @Anno index % 2 == 0 } } """ ) @@ -468,44 +475,12 @@ annotation class B @Test void annotationEntryTrailingComma() { rewriteRun( - spec -> spec.parser(KotlinParser.builder().classpath("jackson-annotations")), kotlin( """ - package org.openrewrite.kotlin - - import com.fasterxml.jackson.`annotation`.JsonTypeInfo - import kotlin.Suppress - import kotlin.collections.List - import kotlin.jvm.JvmName - - @JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "__typename" , // Trailing comma HERE + annotation class Anno ( val a: Int = 1, val b: Int = 2, val c: Int = 3 ) + @Anno( a = 42, b = 42, c = 42 , // Trailing comma HERE ) - public sealed interface Fruit { - @Suppress("INAPPLICABLE_JVM_NAME") - @get : JvmName("getSeeds") - public val seeds: List? - } - """ - ) - ); - } - - @Test - void collectionLiteralExpression() { - rewriteRun( - spec -> spec.parser(KotlinParser.builder().classpath("jackson-annotations")), - kotlin( - """ - import com.fasterxml.jackson.`annotation`.JsonSubTypes - - @JsonSubTypes(value = [ - JsonSubTypes . Type(value = Employee::class, name = "Employee") - ]) - public sealed interface Person - class Employee + class Test """ ) ); @@ -551,9 +526,12 @@ void annotatedFunction() { rewriteRun( kotlin( """ - annotation class Ann + @Target(AnnotationTarget.TYPE) + @Retention(AnnotationRetention.SOURCE) + annotation class Anno + class Foo( - private val option: @Ann () -> Unit + private val option: @Anno () -> Unit ) """ ) @@ -586,11 +564,16 @@ void fieldUseSiteWithMultipleAnnotations() { rewriteRun( kotlin( """ - import javax.inject.Inject - import javax.inject.Named + @Target(AnnotationTarget.FIELD) + @Retention(AnnotationRetention.SOURCE) + annotation class A1 + + @Target(AnnotationTarget.FIELD) + @Retention(AnnotationRetention.SOURCE) + annotation class A2 (val name: String) class Test { - @field : [ Inject Named ( "numberfield " ) ] + @field : [ A1 A2 ( "numberfield " ) ] var field: Long = 0 } """ @@ -604,11 +587,11 @@ void fieldUseSiteWithSingleAnnotationInBracket() { rewriteRun( kotlin( """ - import javax.inject.Inject - + @Target(AnnotationTarget.FIELD) + @Retention(AnnotationRetention.SOURCE) annotation class Anno class A { - @field: [ Inject ] + @field: [ Anno ] var field: Long = 0 } """ @@ -621,11 +604,11 @@ void fieldUseSiteWithSingleAnnotationImplicitBracket() { rewriteRun( kotlin( """ - import javax.inject.Inject - + @Target(AnnotationTarget.FIELD) + @Retention(AnnotationRetention.SOURCE) annotation class Anno class A { - @field : Inject + @field : Anno var field: Long = 0 } """ @@ -637,8 +620,11 @@ class A { @Test void arrayOfCallWithInAnnotation() { rewriteRun( + spec -> spec.typeValidationOptions(TypeValidation.none()), kotlin( """ + @Target(AnnotationTarget.FUNCTION) + @Retention(AnnotationRetention.SOURCE) annotation class Ann( val test: Test ) @@ -661,8 +647,10 @@ void annotatedIntersectionType() { import java.util.* @Target(AnnotationTarget.TYPE) + @Retention(AnnotationRetention.SOURCE) annotation class Anno + @Suppress("UNUSED_PARAMETER") fun < T : Any ? > test( n : Optional < @Anno T & @Anno Any > = Optional.empty < T > ( ) ) { } """ ) diff --git a/src/test/java/org/openrewrite/kotlin/tree/AssignmentOperationTest.java b/src/test/java/org/openrewrite/kotlin/tree/AssignmentOperationTest.java index 3f22cadd5..c734dae33 100644 --- a/src/test/java/org/openrewrite/kotlin/tree/AssignmentOperationTest.java +++ b/src/test/java/org/openrewrite/kotlin/tree/AssignmentOperationTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.openrewrite.Issue; import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; import static org.openrewrite.kotlin.Assertions.kotlin; @@ -120,6 +121,8 @@ fun names(): Set { @Issue("https://github.com/openrewrite/rewrite-kotlin/issues/305") void augmentedAssignmentAnnotation() { rewriteRun( + // Type validation is disabled due to https://github.com/openrewrite/rewrite-kotlin/issues/511 + spec -> spec.typeValidationOptions(TypeValidation.none()), kotlin( """ fun foo(l: MutableList) { diff --git a/src/test/java/org/openrewrite/kotlin/tree/KTSTest.java b/src/test/java/org/openrewrite/kotlin/tree/KTSTest.java index 55807cb2a..c8ca37769 100644 --- a/src/test/java/org/openrewrite/kotlin/tree/KTSTest.java +++ b/src/test/java/org/openrewrite/kotlin/tree/KTSTest.java @@ -19,6 +19,7 @@ import org.openrewrite.java.tree.J; import org.openrewrite.kotlin.KotlinIsoVisitor; import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; import java.util.concurrent.atomic.AtomicInteger; @@ -61,6 +62,7 @@ void topLevelForLoop() { @Test void dslSample() { rewriteRun( + spec -> spec.typeValidationOptions(TypeValidation.none()), kotlinScript( //language=none """ diff --git a/src/test/java/org/openrewrite/kotlin/tree/MethodInvocationTest.java b/src/test/java/org/openrewrite/kotlin/tree/MethodInvocationTest.java index c0113e1af..88287d0de 100644 --- a/src/test/java/org/openrewrite/kotlin/tree/MethodInvocationTest.java +++ b/src/test/java/org/openrewrite/kotlin/tree/MethodInvocationTest.java @@ -20,6 +20,7 @@ import org.openrewrite.java.tree.J; import org.openrewrite.kotlin.marker.IndexedAccess; import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; import static org.assertj.core.api.Assertions.assertThat; import static org.openrewrite.kotlin.Assertions.kotlin; @@ -426,6 +427,7 @@ fun fooBar ( ) { } @Test void unresolvedMethodInvocationName() { rewriteRun( + spec -> spec.typeValidationOptions(TypeValidation.none()), kotlin( """ val x = some . qualified . fooBar ( ) diff --git a/src/test/java/org/openrewrite/kotlin/tree/VariableDeclarationTest.java b/src/test/java/org/openrewrite/kotlin/tree/VariableDeclarationTest.java index 812cbfe01..2fd1a642d 100644 --- a/src/test/java/org/openrewrite/kotlin/tree/VariableDeclarationTest.java +++ b/src/test/java/org/openrewrite/kotlin/tree/VariableDeclarationTest.java @@ -25,6 +25,7 @@ import org.openrewrite.kotlin.KotlinParser; import org.openrewrite.kotlin.marker.Implicit; import org.openrewrite.test.RewriteTest; +import org.openrewrite.test.TypeValidation; import java.util.Objects; @@ -393,6 +394,7 @@ void genericIntersectionType() { @Test void unresolvedNameFirSource() { rewriteRun( + spec -> spec.typeValidationOptions(TypeValidation.none()), kotlin( //language=none, disabled due to invalid code """