All Downloads are FREE. Search and download functionalities are using the official Maven repository.

main.org.openrewrite.kotlin.Assertions Maven / Gradle / Ivy

There is a newer version: 1.22.1
Show newest version
/*
 * Copyright 2022 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.intellij.lang.annotations.Language; import org.jspecify.annotations.Nullable; import org.openrewrite.*; import org.openrewrite.internal.ThrowingConsumer; import org.openrewrite.java.search.FindMissingTypes; import org.openrewrite.java.tree.*; 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.*; 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"}) 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); } } // A helper method to adjust white spaces in the input kotlin source to help us detect parse-to-print idempotent issues // The idea is to differentiate adjacent spaces by whitespace count so that if any adjacent spaces are swapped or unhandled well by the parser, it can be detected. // Just change from `before` to `adjustSpaces(before)` below in the `kotlin()` method to test locally @SuppressWarnings("IfStatementWithIdenticalBranches") private static @Nullable String adjustSpaces(@Nullable String input) { if (input == null) { return null; } StringBuilder out = new StringBuilder(); int count = 0; int limit = 1; char pre = 0; for (char c : input.toCharArray()) { if (c == ' ') { if (pre == ' ') { count++; if (count <= limit) { out.append(c); } } else { count++; out.append(c); } } else { if (pre == ' ') { for (int i = count; i < limit; i++) { out.append(' '); } count = 0; limit++; if (limit > 5) { limit = 1; } } out.append(c); } pre = c; } return out.toString(); } public static SourceSpecs kotlin(@Language("kotlin") @Nullable String before) { // Change `before` to `adjustSpaces(before)` to test spaces locally here return kotlin(before, s -> { }); } public static SourceSpecs kotlinScript(@Language("kts") @Nullable String before) { return kotlinScript(before, s -> { }); } public static SourceSpecs kotlin(@Language("kotlin") @Nullable String before, Consumer> spec) { SourceSpec kotlin = new SourceSpec<>( K.CompilationUnit.class, null, KotlinParser.builder(), before, Assertions::validateTypes, Assertions::customizeExecutionContext ); acceptSpec(spec, kotlin); return kotlin; } public static SourceSpecs kotlinScript(@Language("kts") @Nullable String before, Consumer> spec) { SourceSpec kotlinScript = new SourceSpec<>( K.CompilationUnit.class, null, KotlinParser.builder().isKotlinScript(true), before, Assertions::validateTypes, Assertions::customizeExecutionContext ); acceptSpec(spec, kotlinScript); return kotlinScript; } public static SourceSpecs kotlin(@Language("kotlin") @Nullable String before, @Language("kotlin") String after) { return kotlin(before, after, s -> { }); } 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, Assertions::validateTypes, Assertions::customizeExecutionContext).after(s -> after); acceptSpec(spec, kotlin); return kotlin; } public static SourceSpecs srcMainKotlin(Consumer> spec, SourceSpecs... kotlinSources) { return dir("src/main/kotlin", spec, kotlinSources); } public static SourceSpecs srcMainKotlin(SourceSpecs... kotlinSources) { return srcMainKotlin(spec -> sourceSet(spec, "main"), kotlinSources); } public static SourceSpecs srcTestKotlin(Consumer> spec, SourceSpecs... kotlinSources) { return dir("src/test/kotlin", spec, kotlinSources); } public static SourceSpecs srcTestKotlin(SourceSpecs... kotlinSources) { return srcTestKotlin(spec -> sourceSet(spec, "test"), kotlinSources); } private static void acceptSpec(Consumer> spec, SourceSpec kotlin) { // Consumer consumer = kotlin.getAfterRecipe().andThen(isFullyParsed()).andThen(spaceConscious(kotlin)); Consumer consumer = kotlin.getAfterRecipe().andThen(isFullyParsed()); kotlin.afterRecipe(consumer::accept); spec.accept(kotlin); } public static ThrowingConsumer isFullyParsed() { return cu -> new KotlinIsoVisitor() { @Override public J visitUnknownSource(J.Unknown.Source source, Integer integer) { Optional result = source.getMarkers().findFirst(ParseExceptionResult.class); if (result.isPresent()) { System.out.println(result.get().getMessage()); throw new AssertionFailedError("Parsing error, J.Unknown detected"); } else { throw new UnsupportedOperationException("A J.Unknown should always have a parse exception result."); } } @Override public Space visitSpace(Space space, Space.Location loc, Integer integer) { if (!space.getWhitespace().trim().isEmpty()) { throw new AssertionFailedError("Parsing error detected, whitespace contains non-whitespace characters: " + space.getWhitespace()); } return super.visitSpace(space, loc, integer); } }.visit(cu, 0); } public static UncheckedConsumer> spaceConscious() { return source -> { if (source.getSourceFileType() == K.CompilationUnit.class) { SourceSpec kotlinSourceSpec = (SourceSpec) source; kotlinSourceSpec.afterRecipe(spaceConscious(kotlinSourceSpec)); } }; } 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() { int id = 0; @Override public Space visitSpace(Space space, KSpace.Location loc, Integer integer) { return next(space); } private Space next(Space space) { if (!space.getComments().isEmpty()) { return space; } return space.withComments(Collections.singletonList(new TextComment(true, Integer.toString(id++), "", Markers.EMPTY))); } @Override public Space visitSpace(Space space, Space.Location loc, Integer integer) { Cursor parentCursor = getCursor().getParentOrThrow(); if (loc == Space.Location.IDENTIFIER_PREFIX && parentCursor.getValue() instanceof J.Annotation) { return space; } else if (loc == Space.Location.IDENTIFIER_PREFIX && parentCursor.getValue() instanceof J.Break && ((J.Break) parentCursor.getValue()).getLabel() == getCursor().getValue()) { return space; } else if (loc == Space.Location.IDENTIFIER_PREFIX && parentCursor.getValue() instanceof K.Return && ((K.Return) parentCursor.getValue()).getLabel() == getCursor().getValue()) { return space; } else if (loc == Space.Location.LABEL_SUFFIX) { return space; } else if (getCursor().firstEnclosing(J.Import.class) != null) { return space; } else if (getCursor().firstEnclosing(J.Package.class) != null) { return space; } return next(space); } }.visit(cu, 0); try { String s = visited.printAll(); InMemoryExecutionContext ctx = new InMemoryExecutionContext(); ctx.putMessage(ExecutionContext.REQUIRE_PRINT_EQUALS_INPUT, false); SourceFile cu2 = spec.getParser().build().parse(ctx, s).findFirst().get(); String s1 = cu2.printAll(); assertEquals(s, s1, "Parser is not whitespace print idempotent"); } catch (Exception e) { fail(e); } }; } 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() || isUseSite(); } 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 isUseSite() { Tree value = getCursor().getParentTreeCursor().getValue(); return value instanceof K.AnnotationType || value instanceof K.MultiAnnotationType; } 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 instanceof K.Return); } 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()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy