Extract tests-common module without any actual tests
The main reasoning for the module is to avoid running any compiler tests while executing run configuration that searches tests across module dependencies.
This commit is contained in:
@@ -1,558 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Lists;
|
||||
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.incremental.components.NoLookupLocation;
|
||||
import org.jetbrains.kotlin.resolve.DescriptorUtils;
|
||||
import org.jetbrains.kotlin.resolve.scopes.MemberScope;
|
||||
import org.jetbrains.kotlin.types.KotlinType;
|
||||
import org.junit.Assert;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class DescriptorValidator {
|
||||
|
||||
public static void validate(@NotNull ValidationVisitor validationStrategy, DeclarationDescriptor descriptor) {
|
||||
DiagnosticCollectorForTests collector = new DiagnosticCollectorForTests();
|
||||
validate(validationStrategy, descriptor, collector);
|
||||
collector.done();
|
||||
}
|
||||
|
||||
public static void validate(
|
||||
@NotNull ValidationVisitor validator,
|
||||
@NotNull DeclarationDescriptor descriptor,
|
||||
@NotNull DiagnosticCollector collector
|
||||
) {
|
||||
RecursiveDescriptorProcessor.process(descriptor, collector, validator);
|
||||
}
|
||||
|
||||
private static void report(@NotNull DiagnosticCollector collector, @NotNull DeclarationDescriptor descriptor, @NotNull String message) {
|
||||
collector.report(new ValidationDiagnostic(descriptor, message));
|
||||
}
|
||||
|
||||
public interface DiagnosticCollector {
|
||||
void report(@NotNull ValidationDiagnostic diagnostic);
|
||||
}
|
||||
|
||||
public static class ValidationVisitor implements DeclarationDescriptorVisitor<Boolean, DiagnosticCollector> {
|
||||
public static ValidationVisitor errorTypesForbidden() {
|
||||
return new ValidationVisitor();
|
||||
}
|
||||
|
||||
public static ValidationVisitor errorTypesAllowed() {
|
||||
return new ValidationVisitor().allowErrorTypes();
|
||||
}
|
||||
|
||||
private boolean allowErrorTypes = false;
|
||||
private Predicate<DeclarationDescriptor> recursiveFilter = Predicates.alwaysTrue();
|
||||
|
||||
protected ValidationVisitor() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public ValidationVisitor withStepIntoFilter(@NotNull Predicate<DeclarationDescriptor> filter) {
|
||||
this.recursiveFilter = filter;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public ValidationVisitor allowErrorTypes() {
|
||||
this.allowErrorTypes = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected void validateScope(DeclarationDescriptor scopeOwner, @NotNull MemberScope scope, @NotNull DiagnosticCollector collector) {
|
||||
for (DeclarationDescriptor descriptor : DescriptorUtils.getAllDescriptors(scope)) {
|
||||
if (recursiveFilter.apply(descriptor)) {
|
||||
descriptor.accept(new ScopeValidatorVisitor(collector), scope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateType(
|
||||
@NotNull DeclarationDescriptor descriptor,
|
||||
@Nullable KotlinType type,
|
||||
@NotNull DiagnosticCollector collector
|
||||
) {
|
||||
if (type == null) {
|
||||
report(collector, descriptor, "No type");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!allowErrorTypes && type.isError()) {
|
||||
report(collector, descriptor, "Error type: " + type);
|
||||
return;
|
||||
}
|
||||
|
||||
validateScope(descriptor, type.getMemberScope(), collector);
|
||||
}
|
||||
|
||||
private void validateReturnType(CallableDescriptor descriptor, DiagnosticCollector collector) {
|
||||
validateType(descriptor, descriptor.getReturnType(), collector);
|
||||
}
|
||||
|
||||
private static void validateTypeParameters(DiagnosticCollector collector, List<TypeParameterDescriptor> parameters) {
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
TypeParameterDescriptor typeParameterDescriptor = parameters.get(i);
|
||||
if (typeParameterDescriptor.getIndex() != i) {
|
||||
report(collector, typeParameterDescriptor, "Incorrect index: " + typeParameterDescriptor.getIndex() + " but must be " + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateValueParameters(DiagnosticCollector collector, List<ValueParameterDescriptor> parameters) {
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
ValueParameterDescriptor valueParameterDescriptor = parameters.get(i);
|
||||
if (valueParameterDescriptor.getIndex() != i) {
|
||||
report(collector, valueParameterDescriptor, "Incorrect index: " + valueParameterDescriptor.getIndex() + " but must be " + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateTypes(
|
||||
DeclarationDescriptor descriptor,
|
||||
DiagnosticCollector collector,
|
||||
Collection<KotlinType> types
|
||||
) {
|
||||
for (KotlinType type : types) {
|
||||
validateType(descriptor, type, collector);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateCallable(CallableDescriptor descriptor, DiagnosticCollector collector) {
|
||||
validateReturnType(descriptor, collector);
|
||||
validateTypeParameters(collector, descriptor.getTypeParameters());
|
||||
validateValueParameters(collector, descriptor.getValueParameters());
|
||||
}
|
||||
|
||||
private static <T> void assertEquals(
|
||||
DeclarationDescriptor descriptor,
|
||||
DiagnosticCollector collector,
|
||||
String name,
|
||||
T expected,
|
||||
T actual
|
||||
) {
|
||||
if (!expected.equals(actual)) {
|
||||
report(collector, descriptor, "Wrong " + name + ": " + actual + " must be " + expected);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void assertEqualTypes(
|
||||
DeclarationDescriptor descriptor,
|
||||
DiagnosticCollector collector,
|
||||
String name,
|
||||
KotlinType expected,
|
||||
KotlinType actual
|
||||
) {
|
||||
if (expected.isError() && actual.isError()) {
|
||||
assertEquals(descriptor, collector, name, expected.toString(), actual.toString());
|
||||
}
|
||||
else if (!expected.equals(actual)) {
|
||||
report(collector, descriptor, "Wrong " + name + ": " + actual + " must be " + expected);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateAccessor(
|
||||
PropertyDescriptor descriptor,
|
||||
DiagnosticCollector collector,
|
||||
PropertyAccessorDescriptor accessor,
|
||||
String name
|
||||
) {
|
||||
// TODO: fix the discrepancies in descriptor construction and enable these checks
|
||||
//assertEquals(accessor, collector, name + " visibility", descriptor.getVisibility(), accessor.getVisibility());
|
||||
//assertEquals(accessor, collector, name + " modality", descriptor.getModality(), accessor.getModality());
|
||||
assertEquals(accessor, collector, "corresponding property", descriptor, accessor.getCorrespondingProperty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitPackageFragmentDescriptor(
|
||||
PackageFragmentDescriptor descriptor, DiagnosticCollector collector
|
||||
) {
|
||||
validateScope(descriptor, descriptor.getMemberScope(), collector);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitPackageViewDescriptor(PackageViewDescriptor descriptor, DiagnosticCollector collector) {
|
||||
if (!recursiveFilter.apply(descriptor)) return false;
|
||||
|
||||
validateScope(descriptor, descriptor.getMemberScope(), collector);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitVariableDescriptor(
|
||||
VariableDescriptor descriptor, DiagnosticCollector collector
|
||||
) {
|
||||
validateReturnType(descriptor, collector);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitFunctionDescriptor(
|
||||
FunctionDescriptor descriptor, DiagnosticCollector collector
|
||||
) {
|
||||
validateCallable(descriptor, collector);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitTypeParameterDescriptor(
|
||||
TypeParameterDescriptor descriptor, DiagnosticCollector collector
|
||||
) {
|
||||
validateTypes(descriptor, collector, descriptor.getUpperBounds());
|
||||
|
||||
validateType(descriptor, descriptor.getDefaultType(), collector);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitClassDescriptor(
|
||||
ClassDescriptor descriptor, DiagnosticCollector collector
|
||||
) {
|
||||
validateTypeParameters(collector, descriptor.getDeclaredTypeParameters());
|
||||
|
||||
Collection<KotlinType> supertypes = descriptor.getTypeConstructor().getSupertypes();
|
||||
if (supertypes.isEmpty() && descriptor.getKind() != ClassKind.INTERFACE
|
||||
&& !KotlinBuiltIns.isSpecialClassWithNoSupertypes(descriptor)) {
|
||||
report(collector, descriptor, "No supertypes for non-trait");
|
||||
}
|
||||
validateTypes(descriptor, collector, supertypes);
|
||||
|
||||
validateType(descriptor, descriptor.getDefaultType(), collector);
|
||||
|
||||
validateScope(descriptor, descriptor.getUnsubstitutedInnerClassesScope(), collector);
|
||||
|
||||
List<ConstructorDescriptor> primary = Lists.newArrayList();
|
||||
for (ConstructorDescriptor constructorDescriptor : descriptor.getConstructors()) {
|
||||
if (constructorDescriptor.isPrimary()) {
|
||||
primary.add(constructorDescriptor);
|
||||
}
|
||||
}
|
||||
if (primary.size() > 1) {
|
||||
report(collector, descriptor, "Many primary constructors: " + primary);
|
||||
}
|
||||
|
||||
ConstructorDescriptor primaryConstructor = descriptor.getUnsubstitutedPrimaryConstructor();
|
||||
if (primaryConstructor != null) {
|
||||
if (!descriptor.getConstructors().contains(primaryConstructor)) {
|
||||
report(collector, primaryConstructor,
|
||||
"Primary constructor not in getConstructors() result: " + descriptor.getConstructors());
|
||||
}
|
||||
}
|
||||
|
||||
ClassDescriptor companionObjectDescriptor = descriptor.getCompanionObjectDescriptor();
|
||||
if (companionObjectDescriptor != null && !companionObjectDescriptor.isCompanionObject()) {
|
||||
report(collector, companionObjectDescriptor, "Companion object should be marked as such");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitModuleDeclaration(
|
||||
ModuleDescriptor descriptor, DiagnosticCollector collector
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitConstructorDescriptor(
|
||||
ConstructorDescriptor constructorDescriptor, DiagnosticCollector collector
|
||||
) {
|
||||
visitFunctionDescriptor(constructorDescriptor, collector);
|
||||
|
||||
assertEqualTypes(constructorDescriptor, collector,
|
||||
"return type",
|
||||
constructorDescriptor.getContainingDeclaration().getDefaultType(),
|
||||
constructorDescriptor.getReturnType());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitScriptDescriptor(
|
||||
ScriptDescriptor scriptDescriptor, DiagnosticCollector collector
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitPropertyDescriptor(
|
||||
PropertyDescriptor descriptor, DiagnosticCollector collector
|
||||
) {
|
||||
validateCallable(descriptor, collector);
|
||||
|
||||
PropertyGetterDescriptor getter = descriptor.getGetter();
|
||||
if (getter != null) {
|
||||
assertEqualTypes(getter, collector, "getter return type", descriptor.getType(), getter.getReturnType());
|
||||
validateAccessor(descriptor, collector, getter, "getter");
|
||||
}
|
||||
|
||||
PropertySetterDescriptor setter = descriptor.getSetter();
|
||||
if (setter != null) {
|
||||
assertEquals(setter, collector, "setter parameter count", 1, setter.getValueParameters().size());
|
||||
assertEqualTypes(setter, collector, "setter parameter type", descriptor.getType(), setter.getValueParameters().get(0).getType());
|
||||
assertEquals(setter, collector, "corresponding property", descriptor, setter.getCorrespondingProperty());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitValueParameterDescriptor(
|
||||
ValueParameterDescriptor descriptor, DiagnosticCollector collector
|
||||
) {
|
||||
return visitVariableDescriptor(descriptor, collector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitPropertyGetterDescriptor(
|
||||
PropertyGetterDescriptor descriptor, DiagnosticCollector collector
|
||||
) {
|
||||
return visitFunctionDescriptor(descriptor, collector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitPropertySetterDescriptor(
|
||||
PropertySetterDescriptor descriptor, DiagnosticCollector collector
|
||||
) {
|
||||
return visitFunctionDescriptor(descriptor, collector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitReceiverParameterDescriptor(
|
||||
ReceiverParameterDescriptor descriptor, DiagnosticCollector collector
|
||||
) {
|
||||
validateType(descriptor, descriptor.getType(), collector);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ScopeValidatorVisitor implements DeclarationDescriptorVisitor<Void, MemberScope> {
|
||||
private final DiagnosticCollector collector;
|
||||
|
||||
public ScopeValidatorVisitor(DiagnosticCollector collector) {
|
||||
this.collector = collector;
|
||||
}
|
||||
|
||||
private void report(DeclarationDescriptor expected, String message) {
|
||||
DescriptorValidator.report(collector, expected, message);
|
||||
}
|
||||
|
||||
private void assertFound(
|
||||
@NotNull MemberScope scope,
|
||||
@NotNull DeclarationDescriptor expected,
|
||||
@Nullable DeclarationDescriptor found,
|
||||
boolean shouldBeSame
|
||||
) {
|
||||
if (found == null) {
|
||||
report(expected, "Not found in " + scope);
|
||||
}
|
||||
if (shouldBeSame ? expected != found : !expected.equals(found)) {
|
||||
report(expected, "Lookup error in " + scope + ": " + found);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertFound(
|
||||
@NotNull MemberScope scope,
|
||||
@NotNull DeclarationDescriptor expected,
|
||||
@NotNull Collection<? extends DeclarationDescriptor> found
|
||||
) {
|
||||
if (!found.contains(expected)) {
|
||||
report(expected, "Not found in " + scope + ": " + found);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitPackageFragmentDescriptor(
|
||||
PackageFragmentDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitPackageViewDescriptor(
|
||||
PackageViewDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitVariableDescriptor(
|
||||
VariableDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
assertFound(scope, descriptor, scope.getContributedVariables(descriptor.getName(), NoLookupLocation.FROM_TEST));
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitFunctionDescriptor(
|
||||
FunctionDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
assertFound(scope, descriptor, scope.getContributedFunctions(descriptor.getName(), NoLookupLocation.FROM_TEST));
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitTypeParameterDescriptor(
|
||||
TypeParameterDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
assertFound(scope, descriptor, scope.getContributedClassifier(descriptor.getName(), NoLookupLocation.FROM_TEST), true);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitClassDescriptor(
|
||||
ClassDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
assertFound(scope, descriptor, scope.getContributedClassifier(descriptor.getName(), NoLookupLocation.FROM_TEST), true);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitModuleDeclaration(
|
||||
ModuleDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
report(descriptor, "Module found in scope: " + scope);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitConstructorDescriptor(
|
||||
ConstructorDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
report(descriptor, "Constructor found in scope: " + scope);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitScriptDescriptor(
|
||||
ScriptDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
report(descriptor, "Script found in scope: " + scope);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitPropertyDescriptor(
|
||||
PropertyDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
return visitVariableDescriptor(descriptor, scope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitValueParameterDescriptor(
|
||||
ValueParameterDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
return visitVariableDescriptor(descriptor, scope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitPropertyGetterDescriptor(
|
||||
PropertyGetterDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
report(descriptor, "Getter found in scope: " + scope);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitPropertySetterDescriptor(
|
||||
PropertySetterDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
report(descriptor, "Setter found in scope: " + scope);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitReceiverParameterDescriptor(
|
||||
ReceiverParameterDescriptor descriptor, MemberScope scope
|
||||
) {
|
||||
report(descriptor, "Receiver parameter found in scope: " + scope);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ValidationDiagnostic {
|
||||
|
||||
private final DeclarationDescriptor descriptor;
|
||||
private final String message;
|
||||
private final Throwable stackTrace;
|
||||
|
||||
private ValidationDiagnostic(@NotNull DeclarationDescriptor descriptor, @NotNull String message) {
|
||||
this.descriptor = descriptor;
|
||||
this.message = message;
|
||||
this.stackTrace = new Throwable();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public DeclarationDescriptor getDescriptor() {
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Throwable getStackTrace() {
|
||||
return stackTrace;
|
||||
}
|
||||
|
||||
public void printStackTrace(@NotNull PrintStream out) {
|
||||
out.println(descriptor);
|
||||
out.println(message);
|
||||
stackTrace.printStackTrace(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return descriptor + " > " + message;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DiagnosticCollectorForTests implements DiagnosticCollector {
|
||||
private boolean errorsFound = false;
|
||||
|
||||
@Override
|
||||
public void report(@NotNull ValidationDiagnostic diagnostic) {
|
||||
diagnostic.printStackTrace(System.err);
|
||||
errorsFound = true;
|
||||
}
|
||||
|
||||
public void done() {
|
||||
if (errorsFound) {
|
||||
Assert.fail("Descriptor validation failed (see messages above)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DescriptorValidator() {}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
/*
|
||||
* 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.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
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.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.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.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
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(
|
||||
new Function1<DescriptorRendererOptions, Unit>() {
|
||||
@Override
|
||||
public Unit invoke(DescriptorRendererOptions 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.setModifiers(DescriptorRendererModifier.ALL);
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
public static final Configuration DONT_INCLUDE_METHODS_OF_OBJECT = new Configuration(false, false, false,
|
||||
Predicates.<DeclarationDescriptor>alwaysTrue(),
|
||||
errorTypesForbidden(), DEFAULT_RENDERER);
|
||||
public static final Configuration RECURSIVE = new Configuration(false, false, true,
|
||||
Predicates.<DeclarationDescriptor>alwaysTrue(),
|
||||
errorTypesForbidden(), DEFAULT_RENDERER);
|
||||
|
||||
public static final Configuration RECURSIVE_ALL = new Configuration(true, true, true,
|
||||
Predicates.<DeclarationDescriptor>alwaysTrue(),
|
||||
errorTypesForbidden(), DEFAULT_RENDERER);
|
||||
|
||||
public static final Predicate<DeclarationDescriptor> SKIP_BUILT_INS_PACKAGES = new Predicate<DeclarationDescriptor>() {
|
||||
@Override
|
||||
public boolean apply(DeclarationDescriptor 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
|
||||
) {
|
||||
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.<DeclarationDescriptor>emptyList(), printer);
|
||||
}
|
||||
}
|
||||
else if (descriptor instanceof PackageFragmentDescriptor) {
|
||||
appendSubDescriptors(descriptor, module,
|
||||
((PackageFragmentDescriptor) descriptor).getMemberScope(),
|
||||
Collections.<DeclarationDescriptor>emptyList(), printer);
|
||||
}
|
||||
else if (descriptor instanceof PackageViewDescriptor) {
|
||||
appendSubDescriptors(descriptor, module,
|
||||
((PackageViewDescriptor) descriptor).getMemberScope(),
|
||||
Collections.<DeclarationDescriptor>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();
|
||||
}
|
||||
}
|
||||
|
||||
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.apply(subDescriptor);
|
||||
}
|
||||
|
||||
private void appendSubDescriptors(
|
||||
@NotNull DeclarationDescriptor descriptor,
|
||||
@NotNull ModuleDescriptor module,
|
||||
@NotNull MemberScope memberScope,
|
||||
@NotNull Collection<? extends DeclarationDescriptor> extraSubDescriptors,
|
||||
@NotNull Printer printer
|
||||
) {
|
||||
if (!module.equals(DescriptorUtils.getContainingModule(descriptor))) {
|
||||
printer.println(String.format("// -- Module: %s --", DescriptorUtils.getContainingModule(descriptor).getName()));
|
||||
return;
|
||||
}
|
||||
|
||||
List<DeclarationDescriptor> subDescriptors = Lists.newArrayList();
|
||||
|
||||
subDescriptors.addAll(DescriptorUtils.getAllDescriptors(memberScope));
|
||||
subDescriptors.addAll(extraSubDescriptors);
|
||||
|
||||
Collections.sort(subDescriptors, 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 Predicate<DeclarationDescriptor> recursiveFilter;
|
||||
private final DescriptorRenderer renderer;
|
||||
|
||||
private final DescriptorValidator.ValidationVisitor validationStrategy;
|
||||
|
||||
public Configuration(
|
||||
boolean checkPrimaryConstructors,
|
||||
boolean checkPropertyAccessors,
|
||||
boolean includeMethodsOfKotlinAny,
|
||||
Predicate<DeclarationDescriptor> recursiveFilter,
|
||||
DescriptorValidator.ValidationVisitor validationStrategy,
|
||||
DescriptorRenderer renderer
|
||||
) {
|
||||
this.checkPrimaryConstructors = checkPrimaryConstructors;
|
||||
this.checkPropertyAccessors = checkPropertyAccessors;
|
||||
this.includeMethodsOfKotlinAny = includeMethodsOfKotlinAny;
|
||||
this.recursiveFilter = recursiveFilter;
|
||||
this.validationStrategy = validationStrategy;
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
public Configuration filterRecursion(@NotNull Predicate<DeclarationDescriptor> stepIntoFilter) {
|
||||
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny, stepIntoFilter,
|
||||
validationStrategy.withStepIntoFilter(stepIntoFilter), renderer);
|
||||
}
|
||||
|
||||
public Configuration checkPrimaryConstructors(boolean checkPrimaryConstructors) {
|
||||
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny, recursiveFilter,
|
||||
validationStrategy, renderer);
|
||||
}
|
||||
|
||||
public Configuration checkPropertyAccessors(boolean checkPropertyAccessors) {
|
||||
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny, recursiveFilter,
|
||||
validationStrategy, renderer);
|
||||
}
|
||||
|
||||
public Configuration includeMethodsOfKotlinAny(boolean includeMethodsOfKotlinAny) {
|
||||
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny, recursiveFilter,
|
||||
validationStrategy, renderer);
|
||||
}
|
||||
|
||||
public Configuration withValidationStrategy(@NotNull DescriptorValidator.ValidationVisitor validationStrategy) {
|
||||
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny, recursiveFilter,
|
||||
validationStrategy, renderer);
|
||||
}
|
||||
|
||||
public Configuration withRenderer(@NotNull DescriptorRenderer renderer) {
|
||||
return new Configuration(checkPrimaryConstructors, checkPropertyAccessors, includeMethodsOfKotlinAny, recursiveFilter,
|
||||
validationStrategy, renderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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 org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.kotlin.descriptors.*;
|
||||
import org.jetbrains.kotlin.name.FqName;
|
||||
import org.jetbrains.kotlin.resolve.DescriptorUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class RecursiveDescriptorProcessor {
|
||||
|
||||
public static <D> boolean process(
|
||||
@NotNull DeclarationDescriptor descriptor,
|
||||
D data,
|
||||
@NotNull DeclarationDescriptorVisitor<Boolean, D> visitor
|
||||
) {
|
||||
return descriptor.accept(new RecursiveVisitor<D>(visitor), data);
|
||||
}
|
||||
|
||||
private static class RecursiveVisitor<D> implements DeclarationDescriptorVisitor<Boolean, D> {
|
||||
|
||||
private final DeclarationDescriptorVisitor<Boolean, D> worker;
|
||||
|
||||
private RecursiveVisitor(@NotNull DeclarationDescriptorVisitor<Boolean, D> worker) {
|
||||
this.worker = worker;
|
||||
}
|
||||
|
||||
private boolean visitChildren(Collection<? extends DeclarationDescriptor> descriptors, D data) {
|
||||
for (DeclarationDescriptor descriptor : descriptors) {
|
||||
if (!descriptor.accept(this, data)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean visitChildren(@Nullable DeclarationDescriptor descriptor, D data) {
|
||||
if (descriptor == null) return true;
|
||||
|
||||
return descriptor.accept(this, data);
|
||||
}
|
||||
|
||||
private boolean applyWorker(@NotNull DeclarationDescriptor descriptor, D data) {
|
||||
return descriptor.accept(worker, data);
|
||||
}
|
||||
|
||||
private boolean processCallable(CallableDescriptor descriptor, D data) {
|
||||
return applyWorker(descriptor, data)
|
||||
&& visitChildren(descriptor.getTypeParameters(), data)
|
||||
&& visitChildren(descriptor.getExtensionReceiverParameter(), data)
|
||||
&& visitChildren(descriptor.getValueParameters(), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitPackageFragmentDescriptor(PackageFragmentDescriptor descriptor, D data) {
|
||||
return applyWorker(descriptor, data)
|
||||
&& visitChildren(DescriptorUtils.getAllDescriptors(descriptor.getMemberScope()), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitPackageViewDescriptor(PackageViewDescriptor descriptor, D data) {
|
||||
return applyWorker(descriptor, data)
|
||||
&& visitChildren(DescriptorUtils.getAllDescriptors(descriptor.getMemberScope()), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitVariableDescriptor(VariableDescriptor descriptor, D data) {
|
||||
return processCallable(descriptor, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitPropertyDescriptor(PropertyDescriptor descriptor, D data) {
|
||||
return processCallable(descriptor, data)
|
||||
&& visitChildren(descriptor.getGetter(), data)
|
||||
&& visitChildren(descriptor.getSetter(), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitFunctionDescriptor(FunctionDescriptor descriptor, D data) {
|
||||
return processCallable(descriptor, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitTypeParameterDescriptor(TypeParameterDescriptor descriptor, D data) {
|
||||
return applyWorker(descriptor, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitClassDescriptor(ClassDescriptor descriptor, D data) {
|
||||
return applyWorker(descriptor, data)
|
||||
&& visitChildren(descriptor.getThisAsReceiverParameter(), data)
|
||||
&& visitChildren(descriptor.getConstructors(), data)
|
||||
&& visitChildren(descriptor.getTypeConstructor().getParameters(), data)
|
||||
&& visitChildren(DescriptorUtils.getAllDescriptors(descriptor.getDefaultType().getMemberScope()), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitModuleDeclaration(ModuleDescriptor descriptor, D data) {
|
||||
return applyWorker(descriptor, data)
|
||||
&& visitChildren(descriptor.getPackage(FqName.ROOT), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitConstructorDescriptor(ConstructorDescriptor constructorDescriptor, D data) {
|
||||
return visitFunctionDescriptor(constructorDescriptor, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitScriptDescriptor(ScriptDescriptor scriptDescriptor, D data) {
|
||||
return visitClassDescriptor(scriptDescriptor, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitValueParameterDescriptor(ValueParameterDescriptor descriptor, D data) {
|
||||
return visitVariableDescriptor(descriptor, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitPropertyGetterDescriptor(PropertyGetterDescriptor descriptor, D data) {
|
||||
return visitFunctionDescriptor(descriptor, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitPropertySetterDescriptor(PropertySetterDescriptor descriptor, D data) {
|
||||
return visitFunctionDescriptor(descriptor, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean visitReceiverParameterDescriptor(ReceiverParameterDescriptor descriptor, D data) {
|
||||
return applyWorker(descriptor, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010-2015 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.intellij.psi.*
|
||||
import com.intellij.psi.util.PsiTreeUtil
|
||||
import com.intellij.util.SmartFMap
|
||||
import org.jetbrains.kotlin.psi.KtDeclaration
|
||||
import org.jetbrains.kotlin.psi.KtPackageDirective
|
||||
import org.jetbrains.kotlin.psi.KtTreeVisitorVoid
|
||||
|
||||
fun String.trimTrailingWhitespacesAndAddNewlineAtEOF(): String =
|
||||
this.split('\n').map { it.trimEnd() }.joinToString(separator = "\n").let {
|
||||
result -> if (result.endsWith("\n")) result else result + "\n"
|
||||
}
|
||||
|
||||
fun PsiFile.findElementByCommentPrefix(commentText: String): PsiElement? =
|
||||
findElementsByCommentPrefix(commentText).keys.singleOrNull()
|
||||
|
||||
fun PsiFile.findElementsByCommentPrefix(prefix: String): Map<PsiElement, String> {
|
||||
var result = SmartFMap.emptyMap<PsiElement, String>()
|
||||
accept(
|
||||
object : KtTreeVisitorVoid() {
|
||||
override fun visitComment(comment: PsiComment) {
|
||||
val commentText = comment.text
|
||||
if (commentText.startsWith(prefix)) {
|
||||
val parent = comment.parent
|
||||
val elementToAdd = when (parent) {
|
||||
is KtDeclaration -> parent
|
||||
is PsiMember -> parent
|
||||
else -> PsiTreeUtil.skipSiblingsForward(
|
||||
comment,
|
||||
PsiWhiteSpace::class.java, PsiComment::class.java, KtPackageDirective::class.java
|
||||
)
|
||||
} as? PsiElement ?: return
|
||||
|
||||
result = result.plus(elementToAdd, commentText.substring(prefix.length).trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user