/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* 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
*
* http://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.jetbrains.kotlin.test.util;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import kotlin.Unit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.builtins.KotlinBuiltIns;
import org.jetbrains.kotlin.descriptors.*;
import org.jetbrains.kotlin.descriptors.impl.SubpackagesScope;
import org.jetbrains.kotlin.jvm.compiler.ExpectedLoadErrorsUtil;
import org.jetbrains.kotlin.name.FqName;
import org.jetbrains.kotlin.renderer.*;
import org.jetbrains.kotlin.resolve.DescriptorUtils;
import org.jetbrains.kotlin.resolve.MemberComparator;
import org.jetbrains.kotlin.resolve.MultiTargetPlatform;
import org.jetbrains.kotlin.resolve.scopes.ChainedMemberScope;
import org.jetbrains.kotlin.resolve.scopes.MemberScope;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.utils.Printer;
import org.junit.Assert;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import static org.jetbrains.kotlin.resolve.DescriptorUtils.isEnumEntry;
import static org.jetbrains.kotlin.test.util.DescriptorValidator.ValidationVisitor.errorTypesForbidden;
public class RecursiveDescriptorComparator {
private static final DescriptorRenderer DEFAULT_RENDERER = DescriptorRenderer.Companion.withOptions(
options -> {
options.setWithDefinedIn(false);
options.setExcludedAnnotationClasses(Collections.singleton(new FqName(ExpectedLoadErrorsUtil.ANNOTATION_CLASS_NAME)));
options.setOverrideRenderingPolicy(OverrideRenderingPolicy.RENDER_OPEN_OVERRIDE);
options.setIncludePropertyConstant(true);
options.setClassifierNamePolicy(ClassifierNamePolicy.FULLY_QUALIFIED.INSTANCE);
options.setVerbose(true);
options.setAnnotationArgumentsRenderingPolicy(AnnotationArgumentsRenderingPolicy.UNLESS_EMPTY);
options.setModifiers(DescriptorRendererModifier.ALL);
return Unit.INSTANCE;
}
);
public static final Configuration DONT_INCLUDE_METHODS_OF_OBJECT = new Configuration(false, false, false, false,
descriptor -> true, errorTypesForbidden(), DEFAULT_RENDERER);
public static final Configuration RECURSIVE = new Configuration(false, false, true, false,
descriptor -> true, errorTypesForbidden(), DEFAULT_RENDERER);
public static final Configuration RECURSIVE_ALL = new Configuration(true, true, true, false,
descriptor -> true, errorTypesForbidden(), DEFAULT_RENDERER);
public static final Predicate<DeclarationDescriptor> SKIP_BUILT_INS_PACKAGES = descriptor -> {
if (descriptor instanceof PackageViewDescriptor) {
return !KotlinBuiltIns.BUILT_INS_PACKAGE_FQ_NAME.equals(((PackageViewDescriptor) descriptor).getFqName());
}
return true;
};
private static final ImmutableSet<String> KOTLIN_ANY_METHOD_NAMES = ImmutableSet.of("equals", "hashCode", "toString");
private final Configuration conf;
public RecursiveDescriptorComparator(@NotNull Configuration conf) {
this.conf = conf;
}
public String serializeRecursively(@NotNull DeclarationDescriptor declarationDescriptor) {
StringBuilder result = new StringBuilder();
appendDeclarationRecursively(declarationDescriptor, DescriptorUtils.getContainingModule(declarationDescriptor),
new Printer(result, 1), true);
return result.toString();
}
private void appendDeclarationRecursively(
@NotNull DeclarationDescriptor descriptor,
@NotNull ModuleDescriptor module,
@NotNull Printer printer,
boolean topLevel
) {
if (!isFromModule(descriptor, module)) return;
boolean isEnumEntry = isEnumEntry(descriptor);
boolean isClassOrPackage =
(descriptor instanceof ClassOrPackageFragmentDescriptor || descriptor instanceof PackageViewDescriptor) && !isEnumEntry;
if (isClassOrPackage && !topLevel) {
printer.println();
}
boolean isPrimaryConstructor = descriptor instanceof ConstructorDescriptor && ((ConstructorDescriptor) descriptor).isPrimary();
printer.print(isPrimaryConstructor && conf.checkPrimaryConstructors ? "/*primary*/ " : "", conf.renderer.render(descriptor));
if (isClassOrPackage) {
if (!topLevel) {
printer.printlnWithNoIndent(" {").pushIndent();
}
else {
printer.println();
printer.println();
}
if (descriptor instanceof ClassDescriptor) {
ClassDescriptor klass = (ClassDescriptor) descriptor;
appendSubDescriptors(descriptor, module,
klass.getDefaultType().getMemberScope(), klass.getConstructors(), printer);
MemberScope staticScope = klass.getStaticScope();
if (!DescriptorUtils.getAllDescriptors(staticScope).isEmpty()) {
printer.println();
printer.println("// Static members");
appendSubDescriptors(descriptor, module, staticScope, Collections.emptyList(), printer);
}
}
else if (descriptor instanceof PackageFragmentDescriptor) {
appendSubDescriptors(descriptor, module,
((PackageFragmentDescriptor) descriptor).getMemberScope(),
Collections.emptyList(), printer);
}
else if (descriptor instanceof PackageViewDescriptor) {
appendSubDescriptors(descriptor, module,
getPackageScopeInModule((PackageViewDescriptor) descriptor, module),
Collections.emptyList(), printer);
}
if (!topLevel) {
printer.popIndent().println("}");
}
}
else if (conf.checkPropertyAccessors && descriptor instanceof PropertyDescriptor) {
printer.printlnWithNoIndent();
printer.pushIndent();
PropertyDescriptor propertyDescriptor = (PropertyDescriptor) descriptor;
PropertyGetterDescriptor getter = propertyDescriptor.getGetter();
if (getter != null) {
printer.println(conf.renderer.render(getter));
}
PropertySetterDescriptor setter = propertyDescriptor.getSetter();
if (setter != null) {
printer.println(conf.renderer.render(setter));
}
printer.popIndent();
}
else {
printer.printlnWithNoIndent();
}
if (isEnumEntry) {
printer.println();
}
}
@NotNull
private MemberScope getPackageScopeInModule(@NotNull PackageViewDescriptor descriptor, @NotNull ModuleDescriptor module) {
// See LazyPackageViewDescriptorImpl#memberScope
List<MemberScope> scopes = new ArrayList<>();
for (PackageFragmentDescriptor fragment : descriptor.getFragments()) {
if (isFromModule(fragment, module)) {
scopes.add(fragment.getMemberScope());
}
}
scopes.add(new SubpackagesScope(module, descriptor.getFqName()));
return ChainedMemberScope.Companion.create("test", scopes);
}
private boolean isFromModule(@NotNull DeclarationDescriptor descriptor, @NotNull ModuleDescriptor module) {
if (conf.renderDeclarationsFromOtherModules) return true;
if (descriptor instanceof PackageViewDescriptor) {
// PackageViewDescriptor does not belong to any module, so we check if one of its containing fragments is in our module
for (PackageFragmentDescriptor fragment : ((PackageViewDescriptor) descriptor).getFragments()) {
if (module.equals(DescriptorUtils.getContainingModule(fragment))) return true;
}
}
// 'header' declarations do not belong to the platform-specific module, even though they participate in the analysis
if (descriptor instanceof MemberDescriptor && ((MemberDescriptor) descriptor).isHeader() &&
module.getCapability(MultiTargetPlatform.CAPABILITY) != MultiTargetPlatform.Common.INSTANCE) return false;
return module.equals(DescriptorUtils.getContainingModule(descriptor));
}
private boolean shouldSkip(@NotNull DeclarationDescriptor subDescriptor) {
boolean isFunctionFromAny = subDescriptor.getContainingDeclaration() instanceof ClassDescriptor
&& subDescriptor instanceof FunctionDescriptor
&& KOTLIN_ANY_METHOD_NAMES.contains(subDescriptor.getName().asString());
return (isFunctionFromAny && !conf.includeMethodsOfKotlinAny) || !conf.recursiveFilter.test(subDescriptor);
}
private void appendSubDescriptors(
@NotNull DeclarationDescriptor descriptor,
@NotNull ModuleDescriptor module,
@NotNull MemberScope memberScope,
@NotNull Collection<? extends DeclarationDescriptor> extraSubDescriptors,
@NotNull Printer printer
) {
if (!isFromModule(descriptor, module)) return;
List<DeclarationDescriptor> subDescriptors = Lists.newArrayList();
subDescriptors.addAll(DescriptorUtils.getAllDescriptors(memberScope));
subDescriptors.addAll(extraSubDescriptors);
subDescriptors.sort(MemberComparator.INSTANCE);
for (DeclarationDescriptor subDescriptor : subDescriptors) {
if (!shouldSkip(subDescriptor)) {
appendDeclarationRecursively(subDescriptor, module, printer, false);
}
}
}
private static void compareDescriptorWithFile(
@NotNull DeclarationDescriptor actual,
@NotNull Configuration configuration,
@NotNull File txtFile
) {
doCompareDescriptors(null, actual, configuration, txtFile);
}
public static void compareDescriptors(
@NotNull DeclarationDescriptor expected,
@NotNull DeclarationDescriptor actual,
@NotNull Configuration configuration,
@Nullable File txtFile
) {
if (expected == actual) {
throw new IllegalArgumentException("Don't invoke this method with expected == actual." +
"Invoke compareDescriptorWithFile() instead.");
}
doCompareDescriptors(expected, actual, configuration, txtFile);
}
public static void validateAndCompareDescriptorWithFile(
@NotNull DeclarationDescriptor actual,
@NotNull Configuration configuration,
@NotNull File txtFile
) {
DescriptorValidator.validate(configuration.validationStrategy, actual);
compareDescriptorWithFile(actual, configuration, txtFile);
}
public static void validateAndCompareDescriptors(
@NotNull DeclarationDescriptor expected,
@NotNull DeclarationDescriptor actual,
@NotNull Configuration configuration,
@Nullable File txtFile
) {
DescriptorValidator.validate(configuration.validationStrategy, expected);
DescriptorValidator.validate(configuration.validationStrategy, actual);
compareDescriptors(expected, actual, configuration, txtFile);
}
private static void doCompareDescriptors(
@Nullable DeclarationDescriptor expected,
@NotNull DeclarationDescriptor actual,
@NotNull Configuration configuration,
@Nullable File txtFile
) {
RecursiveDescriptorComparator comparator = new RecursiveDescriptorComparator(configuration);
String actualSerialized = comparator.serializeRecursively(actual);
if (expected != null) {
String expectedSerialized = comparator.serializeRecursively(expected);
Assert.assertEquals("Expected and actual descriptors differ", expectedSerialized, actualSerialized);
}
if (txtFile != null) {
KotlinTestUtils.assertEqualsToFile(txtFile, actualSerialized);
}
}
public static class Configuration {
private final boolean checkPrimaryConstructors;
private final boolean checkPropertyAccessors;
private final boolean includeMethodsOfKotlinAny;
private final boolean renderDeclarationsFromOtherModules;
private final Predicate<DeclarationDescriptor> recursiveFilter;
private final DescriptorRenderer renderer;
private final DescriptorValidator.ValidationVisitor validationStrategy;
public Configuration(
boolean checkPrimaryConstructors,
boolean checkPropertyAccessors,
boolean includeMethodsOfKotlinAny,
boolean renderDeclarationsFromOtherModules,
Predicate<DeclarationDescriptor> recursiveFilter,
DescriptorValidator.ValidationVisitor validationStrategy,
DescriptorRenderer renderer
) {
this.checkPrimaryConstructors = checkPrimaryConstructors;
this.checkPropertyAccessors = checkPropertyAccessors;
this.includeMethodsOfKotlinAny = includeMethodsOfKotlinAny;
this.renderDeclarationsFromOtherModules = renderDeclarationsFromOtherModules;
this.recursiveFilter = recursiveFilter;
this.validationStrategy = validationStrategy;
this.renderer = renderer;
}
public Configuration filterRecursion(@NotNull Predicate<DeclarationDescriptor> stepIntoFilter) {
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny,
renderDeclarationsFromOtherModules, stepIntoFilter,
validationStrategy.withStepIntoFilter(stepIntoFilter), renderer);
}
public Configuration checkPrimaryConstructors(boolean checkPrimaryConstructors) {
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny,
renderDeclarationsFromOtherModules, recursiveFilter, validationStrategy, renderer);
}
public Configuration checkPropertyAccessors(boolean checkPropertyAccessors) {
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny,
renderDeclarationsFromOtherModules, recursiveFilter, validationStrategy, renderer);
}
public Configuration includeMethodsOfKotlinAny(boolean includeMethodsOfKotlinAny) {
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny,
renderDeclarationsFromOtherModules, recursiveFilter, validationStrategy, renderer);
}
public Configuration renderDeclarationsFromOtherModules(boolean renderDeclarationsFromOtherModules) {
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny,
renderDeclarationsFromOtherModules, recursiveFilter, validationStrategy, renderer);
}
public Configuration withValidationStrategy(@NotNull DescriptorValidator.ValidationVisitor validationStrategy) {
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny,
renderDeclarationsFromOtherModules, recursiveFilter, validationStrategy, renderer);
}
public Configuration withRenderer(@NotNull DescriptorRenderer renderer) {
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny,
renderDeclarationsFromOtherModules, recursiveFilter, validationStrategy, renderer);
}
}
}