Allow to turn the first parameter of a SAM-converted lambda into the receiver (KT-12848)
This commit is contained in:
@@ -1,640 +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.checkers;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.intellij.openapi.util.io.FileUtil;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.search.GlobalSearchScope;
|
||||
import kotlin.TuplesKt;
|
||||
import kotlin.collections.CollectionsKt;
|
||||
import kotlin.collections.MapsKt;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import kotlin.jvm.functions.Function2;
|
||||
import kotlin.text.StringsKt;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.kotlin.analyzer.AnalysisResult;
|
||||
import org.jetbrains.kotlin.analyzer.ModuleContent;
|
||||
import org.jetbrains.kotlin.analyzer.ModuleInfo;
|
||||
import org.jetbrains.kotlin.analyzer.common.DefaultAnalyzerFacade;
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.CliLightClassGenerationSupport;
|
||||
import org.jetbrains.kotlin.cli.jvm.compiler.JvmPackagePartProvider;
|
||||
import org.jetbrains.kotlin.config.CommonConfigurationKeys;
|
||||
import org.jetbrains.kotlin.config.CompilerConfiguration;
|
||||
import org.jetbrains.kotlin.config.LanguageVersionSettings;
|
||||
import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl;
|
||||
import org.jetbrains.kotlin.container.ComponentProvider;
|
||||
import org.jetbrains.kotlin.container.DslKt;
|
||||
import org.jetbrains.kotlin.context.ContextKt;
|
||||
import org.jetbrains.kotlin.context.GlobalContext;
|
||||
import org.jetbrains.kotlin.context.ModuleContext;
|
||||
import org.jetbrains.kotlin.context.SimpleGlobalContext;
|
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor;
|
||||
import org.jetbrains.kotlin.descriptors.ModuleDescriptor;
|
||||
import org.jetbrains.kotlin.descriptors.PackagePartProvider;
|
||||
import org.jetbrains.kotlin.descriptors.PackageViewDescriptor;
|
||||
import org.jetbrains.kotlin.descriptors.impl.CompositePackageFragmentProvider;
|
||||
import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl;
|
||||
import org.jetbrains.kotlin.diagnostics.*;
|
||||
import org.jetbrains.kotlin.frontend.java.di.InjectionKt;
|
||||
import org.jetbrains.kotlin.incremental.components.LookupTracker;
|
||||
import org.jetbrains.kotlin.load.java.lazy.SingleModuleClassResolver;
|
||||
import org.jetbrains.kotlin.name.FqName;
|
||||
import org.jetbrains.kotlin.name.Name;
|
||||
import org.jetbrains.kotlin.name.SpecialNames;
|
||||
import org.jetbrains.kotlin.platform.JvmBuiltIns;
|
||||
import org.jetbrains.kotlin.psi.Call;
|
||||
import org.jetbrains.kotlin.psi.KtElement;
|
||||
import org.jetbrains.kotlin.psi.KtExpression;
|
||||
import org.jetbrains.kotlin.psi.KtFile;
|
||||
import org.jetbrains.kotlin.resolve.*;
|
||||
import org.jetbrains.kotlin.resolve.calls.model.MutableResolvedCall;
|
||||
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall;
|
||||
import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowInfo;
|
||||
import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics;
|
||||
import org.jetbrains.kotlin.resolve.jvm.JavaDescriptorResolver;
|
||||
import org.jetbrains.kotlin.resolve.jvm.TopDownAnalyzerFacadeForJVM;
|
||||
import org.jetbrains.kotlin.resolve.lazy.KotlinCodeAnalyzer;
|
||||
import org.jetbrains.kotlin.resolve.lazy.declarations.FileBasedDeclarationProviderFactory;
|
||||
import org.jetbrains.kotlin.storage.ExceptionTracker;
|
||||
import org.jetbrains.kotlin.storage.LockBasedStorageManager;
|
||||
import org.jetbrains.kotlin.storage.StorageManager;
|
||||
import org.jetbrains.kotlin.test.InTextDirectivesUtils;
|
||||
import org.jetbrains.kotlin.test.KotlinTestUtils;
|
||||
import org.jetbrains.kotlin.test.util.DescriptorValidator;
|
||||
import org.jetbrains.kotlin.test.util.RecursiveDescriptorComparator;
|
||||
import org.jetbrains.kotlin.utils.ExceptionUtilsKt;
|
||||
import org.junit.Assert;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jetbrains.kotlin.diagnostics.Errors.*;
|
||||
import static org.jetbrains.kotlin.test.util.RecursiveDescriptorComparator.RECURSIVE;
|
||||
import static org.jetbrains.kotlin.test.util.RecursiveDescriptorComparator.RECURSIVE_ALL;
|
||||
|
||||
public abstract class AbstractDiagnosticsTest extends BaseDiagnosticsTest {
|
||||
|
||||
private static final Function1<String, String> HASH_SANITIZER = new Function1<String, String>() {
|
||||
@Override
|
||||
public String invoke(String s) {
|
||||
return s.replaceAll("@(\\d)+", "");
|
||||
}
|
||||
};
|
||||
|
||||
private static final ModuleDescriptor.Capability<List<KtFile>> MODULE_FILES = new ModuleDescriptor.Capability<List<KtFile>>("");
|
||||
|
||||
@Override
|
||||
protected void analyzeAndCheck(File testDataFile, List<TestFile> testFiles) {
|
||||
Map<TestModule, List<TestFile>> groupedByModule = CollectionsKt.groupByTo(
|
||||
testFiles,
|
||||
new LinkedHashMap<TestModule, List<TestFile>>(),
|
||||
new Function1<TestFile, TestModule>() {
|
||||
@Override
|
||||
public TestModule invoke(TestFile file) {
|
||||
return file.getModule();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
boolean checkLazyResolveLog = CollectionsKt.any(testFiles, new Function1<TestFile, Boolean>() {
|
||||
@Override
|
||||
public Boolean invoke(TestFile file) {
|
||||
return file.checkLazyLog;
|
||||
}
|
||||
});
|
||||
|
||||
LazyOperationsLog lazyOperationsLog = null;
|
||||
GlobalContext context;
|
||||
|
||||
ExceptionTracker tracker = new ExceptionTracker();
|
||||
if (checkLazyResolveLog) {
|
||||
lazyOperationsLog = new LazyOperationsLog(HASH_SANITIZER);
|
||||
context = new SimpleGlobalContext(
|
||||
new LoggingStorageManager(
|
||||
LockBasedStorageManager.createWithExceptionHandling(tracker),
|
||||
lazyOperationsLog.getAddRecordFunction()
|
||||
),
|
||||
tracker
|
||||
);
|
||||
}
|
||||
else {
|
||||
context = new SimpleGlobalContext(LockBasedStorageManager.createWithExceptionHandling(tracker), tracker);
|
||||
}
|
||||
|
||||
Map<TestModule, ModuleDescriptorImpl> modules = createModules(groupedByModule, context.getStorageManager());
|
||||
Map<TestModule, BindingContext> moduleBindings = new HashMap<TestModule, BindingContext>();
|
||||
|
||||
for (Map.Entry<TestModule, List<TestFile>> entry : groupedByModule.entrySet()) {
|
||||
TestModule testModule = entry.getKey();
|
||||
List<? extends TestFile> testFilesInModule = entry.getValue();
|
||||
|
||||
List<KtFile> jetFiles = getJetFiles(testFilesInModule, true);
|
||||
|
||||
ModuleDescriptorImpl oldModule = modules.get(testModule);
|
||||
|
||||
LanguageVersionSettings languageVersionSettings = loadLanguageVersionSettings(testFilesInModule);
|
||||
ModuleContext moduleContext = ContextKt.withModule(ContextKt.withProject(context, getProject()), oldModule);
|
||||
|
||||
boolean separateModules = groupedByModule.size() == 1 && groupedByModule.keySet().iterator().next() == null;
|
||||
AnalysisResult result = analyzeModuleContents(
|
||||
moduleContext, jetFiles, new CliLightClassGenerationSupport.NoScopeRecordCliBindingTrace(),
|
||||
languageVersionSettings, separateModules
|
||||
);
|
||||
ModuleDescriptorImpl newModule = (ModuleDescriptorImpl) result.getModuleDescriptor();
|
||||
if (oldModule != newModule) {
|
||||
// For common modules, we use DefaultAnalyzerFacade who creates ModuleDescriptor instances by itself
|
||||
// (its API does not support working with a module created beforehand).
|
||||
// So, we should replace the old (effectively discarded) module with the new one everywhere in dependencies.
|
||||
// TODO: dirty hack, refactor this test so that it doesn't create ModuleDescriptor instances
|
||||
modules.put(testModule, newModule);
|
||||
for (ModuleDescriptorImpl module : modules.values()) {
|
||||
@SuppressWarnings("deprecation")
|
||||
ListIterator<ModuleDescriptorImpl> it = module.getTestOnly_AllDependentModules().listIterator();
|
||||
while (it.hasNext()) {
|
||||
if (it.next() == oldModule) {
|
||||
it.set(newModule);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moduleBindings.put(testModule, result.getBindingContext());
|
||||
checkAllResolvedCallsAreCompleted(jetFiles, result.getBindingContext());
|
||||
}
|
||||
|
||||
// We want to always create a test data file (txt) if it was missing,
|
||||
// but don't want to skip the following checks in case this one fails
|
||||
Throwable exceptionFromLazyResolveLogValidation = null;
|
||||
if (checkLazyResolveLog) {
|
||||
exceptionFromLazyResolveLogValidation = checkLazyResolveLog(lazyOperationsLog, testDataFile);
|
||||
}
|
||||
else {
|
||||
File lazyLogFile = getLazyLogFile(testDataFile);
|
||||
assertFalse("No lazy log expected, but found: " + lazyLogFile.getAbsolutePath(), lazyLogFile.exists());
|
||||
}
|
||||
|
||||
Throwable exceptionFromDescriptorValidation = null;
|
||||
try {
|
||||
File expectedFile = new File(FileUtil.getNameWithoutExtension(testDataFile.getAbsolutePath()) + ".txt");
|
||||
validateAndCompareDescriptorWithFile(expectedFile, testFiles, modules);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
exceptionFromDescriptorValidation = e;
|
||||
}
|
||||
|
||||
// main checks
|
||||
boolean ok = true;
|
||||
|
||||
StringBuilder actualText = new StringBuilder();
|
||||
for (TestFile testFile : testFiles) {
|
||||
TestModule module = testFile.getModule();
|
||||
boolean isCommonModule = MultiTargetPlatformKt.getMultiTargetPlatform(modules.get(module)) == MultiTargetPlatform.Common.INSTANCE;
|
||||
ok &= testFile.getActualText(
|
||||
moduleBindings.get(module), actualText,
|
||||
shouldSkipJvmSignatureDiagnostics(groupedByModule) || isCommonModule
|
||||
);
|
||||
}
|
||||
|
||||
Throwable exceptionFromDynamicCallDescriptorsValidation = null;
|
||||
try {
|
||||
File expectedFile = new File(FileUtil.getNameWithoutExtension(testDataFile.getAbsolutePath()) + ".dynamic.txt");
|
||||
checkDynamicCallDescriptors(expectedFile, testFiles);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
exceptionFromDynamicCallDescriptorsValidation = e;
|
||||
}
|
||||
|
||||
KotlinTestUtils.assertEqualsToFile(testDataFile, actualText.toString());
|
||||
|
||||
assertTrue("Diagnostics mismatch. See the output above", ok);
|
||||
|
||||
// now we throw a previously found error, if any
|
||||
if (exceptionFromDescriptorValidation != null) {
|
||||
throw ExceptionUtilsKt.rethrow(exceptionFromDescriptorValidation);
|
||||
}
|
||||
if (exceptionFromLazyResolveLogValidation != null) {
|
||||
throw ExceptionUtilsKt.rethrow(exceptionFromLazyResolveLogValidation);
|
||||
}
|
||||
if (exceptionFromDynamicCallDescriptorsValidation != null) {
|
||||
throw ExceptionUtilsKt.rethrow(exceptionFromDynamicCallDescriptorsValidation);
|
||||
}
|
||||
|
||||
performAdditionalChecksAfterDiagnostics(testDataFile, testFiles, groupedByModule, modules, moduleBindings);
|
||||
}
|
||||
|
||||
protected void performAdditionalChecksAfterDiagnostics(
|
||||
File testDataFile,
|
||||
List<TestFile> testFiles,
|
||||
Map<TestModule, List<TestFile>> moduleFiles,
|
||||
Map<TestModule, ModuleDescriptorImpl> moduleDescriptors,
|
||||
Map<TestModule, BindingContext> moduleBindings
|
||||
) {
|
||||
// To be overridden by diagnostic-like tests.
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private LanguageVersionSettings loadLanguageVersionSettings(List<? extends TestFile> module) {
|
||||
LanguageVersionSettings result = null;
|
||||
for (TestFile file : module) {
|
||||
LanguageVersionSettings current = file.customLanguageVersionSettings;
|
||||
if (current != null) {
|
||||
if (result != null && !result.equals(current)) {
|
||||
Assert.fail(
|
||||
"More than one file in the module has " + BaseDiagnosticsTest.LANGUAGE_DIRECTIVE + " or " +
|
||||
BaseDiagnosticsTest.API_VERSION_DIRECTIVE + " directive specified. " +
|
||||
"This is not supported. Please move all directives into one file"
|
||||
);
|
||||
}
|
||||
result = current;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void checkDynamicCallDescriptors(File expectedFile, List<TestFile> testFiles) {
|
||||
RecursiveDescriptorComparator serializer = new RecursiveDescriptorComparator(RECURSIVE_ALL);
|
||||
|
||||
StringBuilder actualText = new StringBuilder();
|
||||
|
||||
for (TestFile testFile : testFiles) {
|
||||
List<DeclarationDescriptor> dynamicCallDescriptors = testFile.getDynamicCallDescriptors();
|
||||
|
||||
for (DeclarationDescriptor descriptor : dynamicCallDescriptors) {
|
||||
String actualSerialized = serializer.serializeRecursively(descriptor);
|
||||
actualText.append(actualSerialized);
|
||||
}
|
||||
}
|
||||
|
||||
if (actualText.length() != 0 || expectedFile.exists()) {
|
||||
KotlinTestUtils.assertEqualsToFile(expectedFile, actualText.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldSkipJvmSignatureDiagnostics(Map<TestModule, List<TestFile>> groupedByModule) {
|
||||
return groupedByModule.size() > 1;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Throwable checkLazyResolveLog(LazyOperationsLog lazyOperationsLog, File testDataFile) {
|
||||
Throwable exceptionFromLazyResolveLogValidation = null;
|
||||
try {
|
||||
File expectedFile = getLazyLogFile(testDataFile);
|
||||
|
||||
KotlinTestUtils.assertEqualsToFile(
|
||||
expectedFile,
|
||||
lazyOperationsLog.getText(),
|
||||
HASH_SANITIZER
|
||||
);
|
||||
}
|
||||
catch (Throwable e) {
|
||||
exceptionFromLazyResolveLogValidation = e;
|
||||
}
|
||||
return exceptionFromLazyResolveLogValidation;
|
||||
}
|
||||
|
||||
private static File getLazyLogFile(File testDataFile) {
|
||||
return new File(FileUtil.getNameWithoutExtension(testDataFile.getAbsolutePath()) + ".lazy.log");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected AnalysisResult analyzeModuleContents(
|
||||
@NotNull ModuleContext moduleContext,
|
||||
@NotNull List<KtFile> files,
|
||||
@NotNull BindingTrace moduleTrace,
|
||||
@Nullable LanguageVersionSettings languageVersionSettings,
|
||||
boolean separateModules
|
||||
) {
|
||||
CompilerConfiguration configuration;
|
||||
if (languageVersionSettings != null) {
|
||||
configuration = getEnvironment().getConfiguration().copy();
|
||||
configuration.put(CommonConfigurationKeys.LANGUAGE_VERSION_SETTINGS, languageVersionSettings);
|
||||
}
|
||||
else {
|
||||
configuration = getEnvironment().getConfiguration();
|
||||
languageVersionSettings = LanguageVersionSettingsImpl.DEFAULT;
|
||||
}
|
||||
|
||||
// New JavaDescriptorResolver is created for each module, which is good because it emulates different Java libraries for each module,
|
||||
// albeit with same class names
|
||||
// See TopDownAnalyzerFacadeForJVM#analyzeFilesWithJavaIntegration
|
||||
|
||||
// Temporary solution: only use separate module mode in single-module tests because analyzeFilesWithJavaIntegration
|
||||
// only supports creating two modules, whereas there can be more than two in multi-module diagnostic tests
|
||||
// TODO: always use separate module mode, once analyzeFilesWithJavaIntegration can create multiple modules
|
||||
if (separateModules) {
|
||||
return TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
|
||||
moduleContext.getProject(),
|
||||
files,
|
||||
moduleTrace,
|
||||
configuration,
|
||||
new Function1<GlobalSearchScope, PackagePartProvider>() {
|
||||
@Override
|
||||
public PackagePartProvider invoke(GlobalSearchScope scope) {
|
||||
return new JvmPackagePartProvider(getEnvironment(), scope);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ModuleDescriptorImpl moduleDescriptor = (ModuleDescriptorImpl) moduleContext.getModule();
|
||||
|
||||
MultiTargetPlatform platform = MultiTargetPlatformKt.getMultiTargetPlatform(moduleDescriptor);
|
||||
if (platform == MultiTargetPlatform.Common.INSTANCE) {
|
||||
//noinspection unchecked
|
||||
return DefaultAnalyzerFacade.INSTANCE.analyzeFiles(
|
||||
files, moduleDescriptor.getName(), true,
|
||||
MapsKt.mapOf(
|
||||
TuplesKt.to(MultiTargetPlatform.CAPABILITY, MultiTargetPlatform.Common.INSTANCE),
|
||||
TuplesKt.to(MODULE_FILES, files)
|
||||
),
|
||||
new Function2<ModuleInfo, ModuleContent, PackagePartProvider>() {
|
||||
@Override
|
||||
public PackagePartProvider invoke(ModuleInfo info, ModuleContent content) {
|
||||
// TODO
|
||||
return PackagePartProvider.Empty.INSTANCE;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
else if (platform != null) {
|
||||
// TODO: analyze with the correct platform, not always JVM
|
||||
files = CollectionsKt.plus(files, getCommonCodeFilesForPlatformSpecificModule(moduleDescriptor));
|
||||
}
|
||||
|
||||
GlobalSearchScope moduleContentScope = GlobalSearchScope.allScope(moduleContext.getProject());
|
||||
SingleModuleClassResolver moduleClassResolver = new SingleModuleClassResolver();
|
||||
ComponentProvider container = InjectionKt.createContainerForTopDownAnalyzerForJvm(
|
||||
moduleContext,
|
||||
moduleTrace,
|
||||
new FileBasedDeclarationProviderFactory(moduleContext.getStorageManager(), files),
|
||||
moduleContentScope,
|
||||
LookupTracker.Companion.getDO_NOTHING(),
|
||||
new JvmPackagePartProvider(getEnvironment(), moduleContentScope),
|
||||
languageVersionSettings,
|
||||
moduleClassResolver
|
||||
);
|
||||
InjectionKt.initJvmBuiltInsForTopDownAnalysis(container, moduleDescriptor, languageVersionSettings);
|
||||
moduleClassResolver.setResolver(DslKt.getService(container, JavaDescriptorResolver.class));
|
||||
|
||||
moduleDescriptor.initialize(new CompositePackageFragmentProvider(Arrays.asList(
|
||||
DslKt.getService(container, KotlinCodeAnalyzer.class).getPackageFragmentProvider(),
|
||||
DslKt.getService(container, JavaDescriptorResolver.class).getPackageFragmentProvider()
|
||||
)));
|
||||
|
||||
DslKt.getService(container, LazyTopDownAnalyzer.class).analyzeDeclarations(
|
||||
TopDownAnalysisMode.TopLevelDeclarations, files, DataFlowInfo.Companion.getEMPTY()
|
||||
);
|
||||
|
||||
return AnalysisResult.success(moduleTrace.getBindingContext(), moduleDescriptor);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<KtFile> getCommonCodeFilesForPlatformSpecificModule(@NotNull ModuleDescriptorImpl moduleDescriptor) {
|
||||
// We assume that a platform-specific module _implements_ all declarations from common modules which are immediate dependencies.
|
||||
// So we collect all sources from such modules to analyze in the platform-specific module as well
|
||||
@SuppressWarnings("deprecation")
|
||||
List<ModuleDescriptorImpl> dependencies = moduleDescriptor.getTestOnly_AllDependentModules();
|
||||
|
||||
// TODO: diagnostics on common code reported during the platform module analysis should be distinguished somehow
|
||||
// E.g. "<!JVM:PLATFORM_DEFINITION_WITHOUT_DECLARATION!>...<!>
|
||||
List<KtFile> result = new ArrayList<KtFile>(0);
|
||||
for (ModuleDescriptorImpl dependency : dependencies) {
|
||||
if (dependency.getCapability(MultiTargetPlatform.CAPABILITY) == MultiTargetPlatform.Common.INSTANCE) {
|
||||
List<KtFile> files = dependency.getCapability(MODULE_FILES);
|
||||
assert files != null : "MODULE_FILES should have been set for the common module: " + dependency;
|
||||
result.addAll(files);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void validateAndCompareDescriptorWithFile(
|
||||
File expectedFile,
|
||||
List<TestFile> testFiles,
|
||||
Map<TestModule, ModuleDescriptorImpl> modules
|
||||
) {
|
||||
if (CollectionsKt.any(testFiles, new Function1<TestFile, Boolean>() {
|
||||
@Override
|
||||
public Boolean invoke(TestFile file) {
|
||||
return InTextDirectivesUtils.isDirectiveDefined(file.expectedText, "// SKIP_TXT");
|
||||
}
|
||||
})) {
|
||||
assertFalse(".txt file should not exist if SKIP_TXT directive is used: " + expectedFile, expectedFile.exists());
|
||||
return;
|
||||
}
|
||||
|
||||
RecursiveDescriptorComparator comparator = new RecursiveDescriptorComparator(createdAffectedPackagesConfiguration(testFiles, modules.values()));
|
||||
|
||||
boolean isMultiModuleTest = modules.size() != 1;
|
||||
StringBuilder rootPackageText = new StringBuilder();
|
||||
|
||||
for (Iterator<TestModule> module = CollectionsKt.sorted(modules.keySet()).iterator(); module.hasNext(); ) {
|
||||
ModuleDescriptorImpl moduleDescriptor = modules.get(module.next());
|
||||
PackageViewDescriptor aPackage = moduleDescriptor.getPackage(FqName.ROOT);
|
||||
assertFalse(aPackage.isEmpty());
|
||||
|
||||
if (isMultiModuleTest) {
|
||||
rootPackageText.append(String.format("// -- Module: %s --\n", moduleDescriptor.getName()));
|
||||
}
|
||||
|
||||
String actualSerialized = comparator.serializeRecursively(aPackage);
|
||||
rootPackageText.append(actualSerialized);
|
||||
|
||||
if (isMultiModuleTest && module.hasNext()) {
|
||||
rootPackageText.append("\n\n");
|
||||
}
|
||||
}
|
||||
|
||||
int lineCount = StringUtil.getLineBreakCount(rootPackageText);
|
||||
assert lineCount < 1000 :
|
||||
"Rendered descriptors of this test take up " + lineCount + " lines. " +
|
||||
"Please ensure you don't render JRE contents to the .txt file. " +
|
||||
"Such tests are hard to maintain, take long time to execute and are subject to sudden unreviewed changes anyway.";
|
||||
|
||||
KotlinTestUtils.assertEqualsToFile(expectedFile, rootPackageText.toString());
|
||||
}
|
||||
|
||||
private RecursiveDescriptorComparator.Configuration createdAffectedPackagesConfiguration(List<TestFile> testFiles, final Collection<? extends ModuleDescriptor> modules) {
|
||||
final Set<Name> packagesNames = getTopLevelPackagesFromFileList(getJetFiles(testFiles, false));
|
||||
|
||||
Predicate<DeclarationDescriptor> stepIntoFilter = new Predicate<DeclarationDescriptor>() {
|
||||
@Override
|
||||
public boolean apply(DeclarationDescriptor descriptor) {
|
||||
ModuleDescriptor module = DescriptorUtils.getContainingModuleOrNull(descriptor);
|
||||
if (!modules.contains(module)) return false;
|
||||
|
||||
if (descriptor instanceof PackageViewDescriptor) {
|
||||
FqName fqName = ((PackageViewDescriptor) descriptor).getFqName();
|
||||
|
||||
if (fqName.isRoot()) return true;
|
||||
|
||||
Name firstName = fqName.pathSegments().get(0);
|
||||
return packagesNames.contains(firstName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
return RECURSIVE.filterRecursion(stepIntoFilter).withValidationStrategy(DescriptorValidator.ValidationVisitor.errorTypesAllowed());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Set<Name> getTopLevelPackagesFromFileList(@NotNull List<KtFile> files) {
|
||||
Set<Name> shortNames = new LinkedHashSet<Name>();
|
||||
for (KtFile file : files) {
|
||||
List<Name> packageFqNameSegments = file.getPackageFqName().pathSegments();
|
||||
Name name = packageFqNameSegments.isEmpty() ? SpecialNames.ROOT_PACKAGE : packageFqNameSegments.get(0);
|
||||
shortNames.add(name);
|
||||
}
|
||||
return shortNames;
|
||||
}
|
||||
|
||||
private Map<TestModule, ModuleDescriptorImpl> createModules(
|
||||
@NotNull Map<TestModule, List<TestFile>> groupedByModule,
|
||||
@NotNull StorageManager storageManager
|
||||
) {
|
||||
Map<TestModule, ModuleDescriptorImpl> modules = new HashMap<TestModule, ModuleDescriptorImpl>();
|
||||
|
||||
for (TestModule testModule : groupedByModule.keySet()) {
|
||||
ModuleDescriptorImpl module =
|
||||
testModule == null ?
|
||||
createSealedModule(storageManager) :
|
||||
createModule(testModule.getName(), storageManager);
|
||||
|
||||
modules.put(testModule, module);
|
||||
}
|
||||
|
||||
for (TestModule testModule : groupedByModule.keySet()) {
|
||||
if (testModule == null) continue;
|
||||
|
||||
ModuleDescriptorImpl module = modules.get(testModule);
|
||||
List<ModuleDescriptorImpl> dependencies = new ArrayList<ModuleDescriptorImpl>();
|
||||
dependencies.add(module);
|
||||
for (TestModule dependency : testModule.getDependencies()) {
|
||||
dependencies.add(modules.get(dependency));
|
||||
}
|
||||
|
||||
dependencies.add(module.getBuiltIns().getBuiltInsModule());
|
||||
dependencies.addAll(getAdditionalDependencies(module));
|
||||
module.setDependencies(dependencies);
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected List<ModuleDescriptorImpl> getAdditionalDependencies(@NotNull ModuleDescriptorImpl module) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("unchecked")
|
||||
protected ModuleDescriptorImpl createModule(@NotNull String moduleName, @NotNull StorageManager storageManager) {
|
||||
String nameSuffix = StringsKt.substringAfterLast(moduleName, "-", "");
|
||||
MultiTargetPlatform platform =
|
||||
nameSuffix.isEmpty() ? null :
|
||||
nameSuffix.equals("common") ? MultiTargetPlatform.Common.INSTANCE : new MultiTargetPlatform.Specific(nameSuffix);
|
||||
Map capabilities =
|
||||
platform == null
|
||||
? Collections.emptyMap()
|
||||
: Collections.singletonMap(MultiTargetPlatform.CAPABILITY, platform);
|
||||
return new ModuleDescriptorImpl(
|
||||
Name.special("<" + moduleName + ">"), storageManager, new JvmBuiltIns(storageManager), capabilities
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected ModuleDescriptorImpl createSealedModule(@NotNull StorageManager storageManager) {
|
||||
ModuleDescriptorImpl moduleDescriptor = createModule("test-module", storageManager);
|
||||
moduleDescriptor.setDependencies(moduleDescriptor, moduleDescriptor.getBuiltIns().getBuiltInsModule());
|
||||
return moduleDescriptor;
|
||||
}
|
||||
|
||||
private static void checkAllResolvedCallsAreCompleted(@NotNull List<KtFile> jetFiles, @NotNull BindingContext bindingContext) {
|
||||
for (KtFile file : jetFiles) {
|
||||
if (!AnalyzingUtils.getSyntaxErrorRanges(file).isEmpty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ImmutableMap<Call, ResolvedCall<?>> resolvedCallsEntries = bindingContext.getSliceContents(BindingContext.RESOLVED_CALL);
|
||||
for (Map.Entry<Call, ResolvedCall<?>> entry : resolvedCallsEntries.entrySet()) {
|
||||
KtElement element = entry.getKey().getCallElement();
|
||||
ResolvedCall<?> resolvedCall = entry.getValue();
|
||||
|
||||
DiagnosticUtils.LineAndColumn lineAndColumn =
|
||||
DiagnosticUtils.getLineAndColumnInPsiFile(element.getContainingFile(), element.getTextRange());
|
||||
|
||||
assertTrue("Resolved call for '" + element.getText() + "'" + lineAndColumn + " is not completed",
|
||||
((MutableResolvedCall<?>) resolvedCall).isCompleted());
|
||||
}
|
||||
|
||||
checkResolvedCallsInDiagnostics(bindingContext);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "ConstantConditions"})
|
||||
private static void checkResolvedCallsInDiagnostics(BindingContext bindingContext) {
|
||||
Set<DiagnosticFactory1<PsiElement, Collection<? extends ResolvedCall<?>>>> diagnosticsStoringResolvedCalls1 = Sets.newHashSet(
|
||||
OVERLOAD_RESOLUTION_AMBIGUITY, NONE_APPLICABLE, CANNOT_COMPLETE_RESOLVE, UNRESOLVED_REFERENCE_WRONG_RECEIVER,
|
||||
ASSIGN_OPERATOR_AMBIGUITY, ITERATOR_AMBIGUITY);
|
||||
Set<DiagnosticFactory2<KtExpression, ? extends Comparable<? extends Comparable<?>>, Collection<? extends ResolvedCall<?>>>>
|
||||
diagnosticsStoringResolvedCalls2 = Sets.newHashSet(
|
||||
COMPONENT_FUNCTION_AMBIGUITY, DELEGATE_SPECIAL_FUNCTION_AMBIGUITY, DELEGATE_SPECIAL_FUNCTION_NONE_APPLICABLE);
|
||||
Diagnostics diagnostics = bindingContext.getDiagnostics();
|
||||
for (Diagnostic diagnostic : diagnostics) {
|
||||
DiagnosticFactory<?> factory = diagnostic.getFactory();
|
||||
//noinspection SuspiciousMethodCalls
|
||||
if (diagnosticsStoringResolvedCalls1.contains(factory)) {
|
||||
assertResolvedCallsAreCompleted(
|
||||
diagnostic, DiagnosticFactory.cast(diagnostic, diagnosticsStoringResolvedCalls1).getA());
|
||||
}
|
||||
//noinspection SuspiciousMethodCalls
|
||||
if (diagnosticsStoringResolvedCalls2.contains(factory)) {
|
||||
assertResolvedCallsAreCompleted(
|
||||
diagnostic,
|
||||
DiagnosticFactory.cast(diagnostic, diagnosticsStoringResolvedCalls2).getB());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertResolvedCallsAreCompleted(
|
||||
@NotNull Diagnostic diagnostic, @NotNull Collection<? extends ResolvedCall<?>> resolvedCalls
|
||||
) {
|
||||
boolean allCallsAreCompleted = true;
|
||||
for (ResolvedCall<?> resolvedCall : resolvedCalls) {
|
||||
if (!((MutableResolvedCall<?>) resolvedCall).isCompleted()) {
|
||||
allCallsAreCompleted = false;
|
||||
}
|
||||
}
|
||||
|
||||
PsiElement element = diagnostic.getPsiElement();
|
||||
DiagnosticUtils.LineAndColumn lineAndColumn =
|
||||
DiagnosticUtils.getLineAndColumnInPsiFile(element.getContainingFile(), element.getTextRange());
|
||||
|
||||
assertTrue("Resolved calls stored in " + diagnostic.getFactory().getName() + "\n" +
|
||||
"for '" + element.getText() + "'" + lineAndColumn + " are not completed",
|
||||
allCallsAreCompleted);
|
||||
}
|
||||
}
|
||||
@@ -1,524 +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.checkers;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.intellij.lang.java.JavaLanguage;
|
||||
import com.intellij.openapi.util.Condition;
|
||||
import com.intellij.openapi.util.Conditions;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.psi.PsiFileFactory;
|
||||
import com.intellij.psi.search.GlobalSearchScope;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.util.Function;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.intellij.util.containers.HashMap;
|
||||
import kotlin.collections.CollectionsKt;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.kotlin.asJava.DuplicateJvmSignatureUtilKt;
|
||||
import org.jetbrains.kotlin.config.ApiVersion;
|
||||
import org.jetbrains.kotlin.config.LanguageFeature;
|
||||
import org.jetbrains.kotlin.config.LanguageVersionSettings;
|
||||
import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl;
|
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor;
|
||||
import org.jetbrains.kotlin.diagnostics.*;
|
||||
import org.jetbrains.kotlin.load.java.InternalFlexibleTypeTransformer;
|
||||
import org.jetbrains.kotlin.psi.KtDeclaration;
|
||||
import org.jetbrains.kotlin.psi.KtFile;
|
||||
import org.jetbrains.kotlin.resolve.BindingContext;
|
||||
import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics;
|
||||
import org.jetbrains.kotlin.test.KotlinTestUtils;
|
||||
import org.jetbrains.kotlin.utils.StringsKt;
|
||||
import org.junit.Assert;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public abstract class BaseDiagnosticsTest
|
||||
extends KotlinMultiFileTestWithJava<BaseDiagnosticsTest.TestModule, BaseDiagnosticsTest.TestFile> {
|
||||
|
||||
public static final String DIAGNOSTICS_DIRECTIVE = "DIAGNOSTICS";
|
||||
public static final Pattern DIAGNOSTICS_PATTERN = Pattern.compile("([\\+\\-!])(\\w+)\\s*");
|
||||
public static final ImmutableSet<DiagnosticFactory<?>> DIAGNOSTICS_TO_INCLUDE_ANYWAY =
|
||||
ImmutableSet.of(
|
||||
Errors.UNRESOLVED_REFERENCE,
|
||||
Errors.UNRESOLVED_REFERENCE_WRONG_RECEIVER,
|
||||
CheckerTestUtil.SyntaxErrorDiagnosticFactory.INSTANCE,
|
||||
CheckerTestUtil.DebugInfoDiagnosticFactory.ELEMENT_WITH_ERROR_TYPE,
|
||||
CheckerTestUtil.DebugInfoDiagnosticFactory.MISSING_UNRESOLVED,
|
||||
CheckerTestUtil.DebugInfoDiagnosticFactory.UNRESOLVED_WITH_TARGET
|
||||
);
|
||||
|
||||
public static final String LANGUAGE_DIRECTIVE = "LANGUAGE";
|
||||
private static final Pattern LANGUAGE_PATTERN = Pattern.compile("([\\+\\-])(\\w+)\\s*");
|
||||
|
||||
public static final String API_VERSION_DIRECTIVE = "API_VERSION";
|
||||
|
||||
public static final String CHECK_TYPE_DIRECTIVE = "CHECK_TYPE";
|
||||
public static final String CHECK_TYPE_PACKAGE = "tests._checkType";
|
||||
private static final String CHECK_TYPE_DECLARATIONS = "\npackage " + CHECK_TYPE_PACKAGE +
|
||||
"\nfun <T> checkSubtype(t: T) = t" +
|
||||
"\nclass Inv<T>" +
|
||||
"\nfun <E> Inv<E>._() {}" +
|
||||
"\ninfix fun <T> T.checkType(f: Inv<T>.() -> Unit) {}";
|
||||
public static final String CHECK_TYPE_IMPORT = "import " + CHECK_TYPE_PACKAGE + ".*";
|
||||
|
||||
public static final String EXPLICIT_FLEXIBLE_TYPES_DIRECTIVE = "EXPLICIT_FLEXIBLE_TYPES";
|
||||
public static final String EXPLICIT_FLEXIBLE_PACKAGE = InternalFlexibleTypeTransformer.FLEXIBLE_TYPE_CLASSIFIER.getPackageFqName().asString();
|
||||
public static final String EXPLICIT_FLEXIBLE_CLASS_NAME = InternalFlexibleTypeTransformer.FLEXIBLE_TYPE_CLASSIFIER.getRelativeClassName().asString();
|
||||
private static final String EXPLICIT_FLEXIBLE_TYPES_DECLARATIONS
|
||||
= "\npackage " + EXPLICIT_FLEXIBLE_PACKAGE +
|
||||
"\npublic class " + EXPLICIT_FLEXIBLE_CLASS_NAME + "<L, U>";
|
||||
private static final String EXPLICIT_FLEXIBLE_TYPES_IMPORT = "import " + EXPLICIT_FLEXIBLE_PACKAGE + "." + EXPLICIT_FLEXIBLE_CLASS_NAME;
|
||||
public static final String CHECK_LAZY_LOG_DIRECTIVE = "CHECK_LAZY_LOG";
|
||||
public static final boolean CHECK_LAZY_LOG_DEFAULT = "true".equals(System.getProperty("check.lazy.logs", "false"));
|
||||
|
||||
public static final String MARK_DYNAMIC_CALLS_DIRECTIVE = "MARK_DYNAMIC_CALLS";
|
||||
|
||||
@Override
|
||||
protected TestModule createTestModule(@NotNull String name) {
|
||||
return new TestModule(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TestFile createTestFile(TestModule module, String fileName, String text, Map<String, String> directives) {
|
||||
return new TestFile(module, fileName, text, directives);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doMultiFileTest(File file, final Map<String, ModuleAndDependencies> modules, List<TestFile> testFiles) {
|
||||
for (final ModuleAndDependencies moduleAndDependencies : modules.values()) {
|
||||
List<TestModule> dependencies = CollectionsKt.map(
|
||||
moduleAndDependencies.dependencies,
|
||||
new Function1<String, TestModule>() {
|
||||
@Override
|
||||
public TestModule invoke(String name) {
|
||||
ModuleAndDependencies dependency = modules.get(name);
|
||||
assert dependency != null : "Dependency not found: " +
|
||||
name +
|
||||
" for module " +
|
||||
moduleAndDependencies.module.getName();
|
||||
return dependency.module;
|
||||
}
|
||||
}
|
||||
);
|
||||
moduleAndDependencies.module.getDependencies().addAll(dependencies);
|
||||
}
|
||||
|
||||
analyzeAndCheck(file, testFiles);
|
||||
}
|
||||
|
||||
protected abstract void analyzeAndCheck(
|
||||
File testDataFile,
|
||||
List<TestFile> files
|
||||
);
|
||||
|
||||
protected List<KtFile> getJetFiles(List<? extends TestFile> testFiles, boolean includeExtras) {
|
||||
boolean declareFlexibleType = false;
|
||||
boolean declareCheckType = false;
|
||||
List<KtFile> jetFiles = Lists.newArrayList();
|
||||
for (TestFile testFile : testFiles) {
|
||||
if (testFile.getJetFile() != null) {
|
||||
jetFiles.add(testFile.getJetFile());
|
||||
}
|
||||
declareFlexibleType |= testFile.declareFlexibleType;
|
||||
declareCheckType |= testFile.declareCheckType;
|
||||
}
|
||||
|
||||
if (includeExtras) {
|
||||
if (declareFlexibleType) {
|
||||
jetFiles.add(KotlinTestUtils.createFile("EXPLICIT_FLEXIBLE_TYPES.kt", EXPLICIT_FLEXIBLE_TYPES_DECLARATIONS, getProject()));
|
||||
}
|
||||
if (declareCheckType) {
|
||||
jetFiles.add(KotlinTestUtils.createFile("CHECK_TYPE.kt", CHECK_TYPE_DECLARATIONS, getProject()));
|
||||
}
|
||||
}
|
||||
|
||||
return jetFiles;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static LanguageVersionSettings parseLanguageVersionSettings(Map<String, String> directiveMap) {
|
||||
String apiVersionString = directiveMap.get(API_VERSION_DIRECTIVE);
|
||||
String directives = directiveMap.get(LANGUAGE_DIRECTIVE);
|
||||
if (apiVersionString == null && directives == null) return null;
|
||||
|
||||
ApiVersion apiVersion = apiVersionString != null ? ApiVersion.Companion.parse(apiVersionString) : ApiVersion.LATEST;
|
||||
assert apiVersion != null : "Unknown API version: " + apiVersionString;
|
||||
|
||||
Map<LanguageFeature, Boolean> languageFeatures =
|
||||
directives == null ? Collections.<LanguageFeature, Boolean>emptyMap() : collectLanguageFeatureMap(directives);
|
||||
|
||||
return new DiagnosticTestLanguageVersionSettings(languageFeatures, apiVersion);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<LanguageFeature, Boolean> collectLanguageFeatureMap(@NotNull String directives) {
|
||||
Matcher matcher = LANGUAGE_PATTERN.matcher(directives);
|
||||
if (!matcher.find()) {
|
||||
Assert.fail(
|
||||
"Wrong syntax in the '// !" + LANGUAGE_DIRECTIVE + ": ...' directive:\n" +
|
||||
"found: '" + directives + "'\n" +
|
||||
"Must be '([+-]LanguageFeatureName)+'\n" +
|
||||
"where '+' means 'enable' and '-' means 'disable'\n" +
|
||||
"and language feature names are names of enum entries in LanguageFeature enum class"
|
||||
);
|
||||
}
|
||||
|
||||
Map<LanguageFeature, Boolean> values = new HashMap<LanguageFeature, Boolean>();
|
||||
do {
|
||||
boolean enable = matcher.group(1).equals("+");
|
||||
String name = matcher.group(2);
|
||||
LanguageFeature feature = LanguageFeature.fromString(name);
|
||||
if (feature == null) {
|
||||
Assert.fail(
|
||||
"Language feature not found, please check spelling: " + name + "\n" +
|
||||
"Known features:\n " + StringsKt.join(Arrays.asList(LanguageFeature.values()), "\n ")
|
||||
);
|
||||
}
|
||||
if (values.put(feature, enable) != null) {
|
||||
Assert.fail("Duplicate entry for the language feature: " + name);
|
||||
}
|
||||
}
|
||||
while (matcher.find());
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static Condition<Diagnostic> parseDiagnosticFilterDirective(Map<String, String> directiveMap) {
|
||||
String directives = directiveMap.get(DIAGNOSTICS_DIRECTIVE);
|
||||
if (directives == null) {
|
||||
// If "!API_VERSION" is present, disable the NEWER_VERSION_IN_SINCE_KOTLIN diagnostic.
|
||||
// Otherwise it would be reported in any non-trivial test on the @SinceKotlin value.
|
||||
if (directiveMap.containsKey(API_VERSION_DIRECTIVE)) {
|
||||
return new Condition<Diagnostic>() {
|
||||
@Override
|
||||
public boolean value(Diagnostic diagnostic) {
|
||||
return diagnostic.getFactory() != Errors.NEWER_VERSION_IN_SINCE_KOTLIN;
|
||||
}
|
||||
};
|
||||
}
|
||||
return Conditions.alwaysTrue();
|
||||
}
|
||||
Condition<Diagnostic> condition = Conditions.alwaysTrue();
|
||||
Matcher matcher = DIAGNOSTICS_PATTERN.matcher(directives);
|
||||
if (!matcher.find()) {
|
||||
Assert.fail("Wrong syntax in the '// !" + DIAGNOSTICS_DIRECTIVE + ": ...' directive:\n" +
|
||||
"found: '" + directives + "'\n" +
|
||||
"Must be '([+-!]DIAGNOSTIC_FACTORY_NAME|ERROR|WARNING|INFO)+'\n" +
|
||||
"where '+' means 'include'\n" +
|
||||
" '-' means 'exclude'\n" +
|
||||
" '!' means 'exclude everything but this'\n" +
|
||||
"directives are applied in the order of appearance, i.e. !FOO +BAR means include only FOO and BAR");
|
||||
}
|
||||
boolean first = true;
|
||||
do {
|
||||
String operation = matcher.group(1);
|
||||
final String name = matcher.group(2);
|
||||
|
||||
Condition<Diagnostic> newCondition;
|
||||
if (ImmutableSet.of("ERROR", "WARNING", "INFO").contains(name)) {
|
||||
final Severity severity = Severity.valueOf(name);
|
||||
newCondition = new Condition<Diagnostic>() {
|
||||
@Override
|
||||
public boolean value(Diagnostic diagnostic) {
|
||||
return diagnostic.getSeverity() == severity;
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
newCondition = new Condition<Diagnostic>() {
|
||||
@Override
|
||||
public boolean value(Diagnostic diagnostic) {
|
||||
return name.equals(diagnostic.getFactory().getName());
|
||||
}
|
||||
};
|
||||
}
|
||||
if ("!".equals(operation)) {
|
||||
if (!first) {
|
||||
Assert.fail("'" + operation + name + "' appears in a position rather than the first one, " +
|
||||
"which effectively cancels all the previous filters in this directive");
|
||||
}
|
||||
condition = newCondition;
|
||||
}
|
||||
else if ("+".equals(operation)) {
|
||||
condition = Conditions.or(condition, newCondition);
|
||||
}
|
||||
else if ("-".equals(operation)) {
|
||||
condition = Conditions.and(condition, Conditions.not(newCondition));
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
while (matcher.find());
|
||||
// We always include UNRESOLVED_REFERENCE and SYNTAX_ERROR because they are too likely to indicate erroneous test data
|
||||
return Conditions.or(
|
||||
condition,
|
||||
new Condition<Diagnostic>() {
|
||||
@Override
|
||||
public boolean value(Diagnostic diagnostic) {
|
||||
return DIAGNOSTICS_TO_INCLUDE_ANYWAY.contains(diagnostic.getFactory());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected static class TestModule implements Comparable<TestModule> {
|
||||
private final String name;
|
||||
private final List<TestModule> dependencies = new ArrayList<TestModule>();
|
||||
|
||||
public TestModule(@NotNull String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<TestModule> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull TestModule module) {
|
||||
return name.compareTo(module.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName();
|
||||
}
|
||||
}
|
||||
|
||||
public static class DiagnosticTestLanguageVersionSettings implements LanguageVersionSettings {
|
||||
private final Map<LanguageFeature, Boolean> languageFeatures;
|
||||
private final ApiVersion apiVersion;
|
||||
|
||||
public DiagnosticTestLanguageVersionSettings(
|
||||
@NotNull Map<LanguageFeature, Boolean> languageFeatures, @NotNull ApiVersion apiVersion
|
||||
) {
|
||||
this.languageFeatures = languageFeatures;
|
||||
this.apiVersion = apiVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFeature(@NotNull LanguageFeature feature) {
|
||||
Boolean enabled = languageFeatures.get(feature);
|
||||
return enabled != null ? enabled : LanguageVersionSettingsImpl.DEFAULT.supportsFeature(feature);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ApiVersion getApiVersion() {
|
||||
return apiVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof DiagnosticTestLanguageVersionSettings &&
|
||||
((DiagnosticTestLanguageVersionSettings) obj).languageFeatures.equals(languageFeatures) &&
|
||||
((DiagnosticTestLanguageVersionSettings) obj).apiVersion.equals(apiVersion);
|
||||
}
|
||||
}
|
||||
|
||||
protected class TestFile {
|
||||
private final List<CheckerTestUtil.DiagnosedRange> diagnosedRanges = Lists.newArrayList();
|
||||
public final String expectedText;
|
||||
private final TestModule module;
|
||||
private final String clearText;
|
||||
private final KtFile jetFile;
|
||||
private final Condition<Diagnostic> whatDiagnosticsToConsider;
|
||||
public final LanguageVersionSettings customLanguageVersionSettings;
|
||||
private final boolean declareCheckType;
|
||||
private final boolean declareFlexibleType;
|
||||
public final boolean checkLazyLog;
|
||||
private final boolean markDynamicCalls;
|
||||
private final List<DeclarationDescriptor> dynamicCallDescriptors = new ArrayList<DeclarationDescriptor>();
|
||||
|
||||
public TestFile(
|
||||
@Nullable TestModule module,
|
||||
String fileName,
|
||||
String textWithMarkers,
|
||||
Map<String, String> directives
|
||||
) {
|
||||
this.module = module;
|
||||
this.whatDiagnosticsToConsider = parseDiagnosticFilterDirective(directives);
|
||||
this.customLanguageVersionSettings = parseLanguageVersionSettings(directives);
|
||||
this.checkLazyLog = directives.containsKey(CHECK_LAZY_LOG_DIRECTIVE) || CHECK_LAZY_LOG_DEFAULT;
|
||||
this.declareCheckType = directives.containsKey(CHECK_TYPE_DIRECTIVE);
|
||||
this.declareFlexibleType = directives.containsKey(EXPLICIT_FLEXIBLE_TYPES_DIRECTIVE);
|
||||
this.markDynamicCalls = directives.containsKey(MARK_DYNAMIC_CALLS_DIRECTIVE);
|
||||
if (fileName.endsWith(".java")) {
|
||||
PsiFileFactory.getInstance(getProject()).createFileFromText(fileName, JavaLanguage.INSTANCE, textWithMarkers);
|
||||
// TODO: check there's not syntax errors
|
||||
this.jetFile = null;
|
||||
this.expectedText = this.clearText = textWithMarkers;
|
||||
}
|
||||
else {
|
||||
this.expectedText = textWithMarkers;
|
||||
String textWithExtras = addExtras(expectedText);
|
||||
this.clearText = CheckerTestUtil.parseDiagnosedRanges(textWithExtras, diagnosedRanges);
|
||||
this.jetFile = TestCheckerUtil.createCheckAndReturnPsiFile(fileName, clearText, getProject());
|
||||
for (CheckerTestUtil.DiagnosedRange diagnosedRange : diagnosedRanges) {
|
||||
diagnosedRange.setFile(jetFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getImports() {
|
||||
String imports = "";
|
||||
if (declareCheckType) {
|
||||
imports += CHECK_TYPE_IMPORT + "\n";
|
||||
}
|
||||
if (declareFlexibleType) {
|
||||
imports += EXPLICIT_FLEXIBLE_TYPES_IMPORT + "\n";
|
||||
}
|
||||
return imports;
|
||||
}
|
||||
|
||||
private String getExtras() {
|
||||
return "/*extras*/\n" + getImports() + "/*extras*/\n\n";
|
||||
}
|
||||
|
||||
private String addExtras(String text) {
|
||||
return addImports(text, getExtras());
|
||||
}
|
||||
|
||||
private void stripExtras(StringBuilder actualText) {
|
||||
String extras = getExtras();
|
||||
int start = actualText.indexOf(extras);
|
||||
if (start >= 0) {
|
||||
actualText.delete(start, start + extras.length());
|
||||
}
|
||||
}
|
||||
|
||||
private String addImports(String text, String imports) {
|
||||
Pattern pattern = Pattern.compile("^package [\\.\\w\\d]*\n", Pattern.MULTILINE);
|
||||
Matcher matcher = pattern.matcher(text);
|
||||
if (matcher.find()) {
|
||||
// add imports after the package directive
|
||||
text = text.substring(0, matcher.end()) + imports + text.substring(matcher.end());
|
||||
}
|
||||
else {
|
||||
// add imports at the beginning
|
||||
text = imports + text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TestModule getModule() {
|
||||
return module;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public KtFile getJetFile() {
|
||||
return jetFile;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<DeclarationDescriptor> getDynamicCallDescriptors() {
|
||||
return dynamicCallDescriptors;
|
||||
}
|
||||
|
||||
public boolean getActualText(BindingContext bindingContext, StringBuilder actualText, boolean skipJvmSignatureDiagnostics) {
|
||||
if (this.jetFile == null) {
|
||||
// TODO: check java files too
|
||||
actualText.append(this.clearText);
|
||||
return true;
|
||||
}
|
||||
|
||||
Set<Diagnostic> jvmSignatureDiagnostics = skipJvmSignatureDiagnostics
|
||||
? Collections.<Diagnostic>emptySet()
|
||||
: computeJvmSignatureDiagnostics(bindingContext);
|
||||
|
||||
final boolean[] ok = { true };
|
||||
List<Diagnostic> diagnostics = ContainerUtil.filter(
|
||||
CollectionsKt.plus(CheckerTestUtil.getDiagnosticsIncludingSyntaxErrors(bindingContext, jetFile, markDynamicCalls, dynamicCallDescriptors),
|
||||
jvmSignatureDiagnostics),
|
||||
whatDiagnosticsToConsider
|
||||
);
|
||||
|
||||
Map<Diagnostic, CheckerTestUtil.TextDiagnostic> diagnosticToExpectedDiagnostic = ContainerUtil.newHashMap();
|
||||
CheckerTestUtil.diagnosticsDiff(diagnosticToExpectedDiagnostic, diagnosedRanges, diagnostics, new CheckerTestUtil.DiagnosticDiffCallbacks() {
|
||||
|
||||
@Override
|
||||
public void missingDiagnostic(CheckerTestUtil.TextDiagnostic diagnostic, int expectedStart, int expectedEnd) {
|
||||
String message = "Missing " + diagnostic.getName() + DiagnosticUtils.atLocation(jetFile, new TextRange(expectedStart, expectedEnd));
|
||||
System.err.println(message);
|
||||
ok[0] = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void wrongParametersDiagnostic(
|
||||
CheckerTestUtil.TextDiagnostic expectedDiagnostic,
|
||||
CheckerTestUtil.TextDiagnostic actualDiagnostic,
|
||||
int start,
|
||||
int end
|
||||
) {
|
||||
String message = "Parameters of diagnostic not equal at position "
|
||||
+ DiagnosticUtils.atLocation(jetFile, new TextRange(start, end))
|
||||
+ ". Expected: " + expectedDiagnostic.asString() + ", actual: " + actualDiagnostic.asString();
|
||||
System.err.println(message);
|
||||
ok[0] = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unexpectedDiagnostic(CheckerTestUtil.TextDiagnostic diagnostic, int actualStart, int actualEnd) {
|
||||
String message = "Unexpected " + diagnostic.getName() + DiagnosticUtils.atLocation(jetFile, new TextRange(actualStart, actualEnd));
|
||||
System.err.println(message);
|
||||
ok[0] = false;
|
||||
}
|
||||
});
|
||||
|
||||
actualText.append(CheckerTestUtil.addDiagnosticMarkersToText(jetFile, diagnostics, diagnosticToExpectedDiagnostic, new Function<PsiFile, String>() {
|
||||
@Override
|
||||
public String fun(PsiFile file) {
|
||||
return file.getText();
|
||||
}
|
||||
}));
|
||||
|
||||
stripExtras(actualText);
|
||||
|
||||
return ok[0];
|
||||
}
|
||||
|
||||
private Set<Diagnostic> computeJvmSignatureDiagnostics(BindingContext bindingContext) {
|
||||
Set<Diagnostic> jvmSignatureDiagnostics = new HashSet<Diagnostic>();
|
||||
Collection<KtDeclaration> declarations = PsiTreeUtil.findChildrenOfType(jetFile, KtDeclaration.class);
|
||||
for (KtDeclaration declaration : declarations) {
|
||||
Diagnostics diagnostics = DuplicateJvmSignatureUtilKt.getJvmSignatureDiagnostics(declaration, bindingContext.getDiagnostics(),
|
||||
GlobalSearchScope.allScope(getProject()));
|
||||
if (diagnostics == null) continue;
|
||||
jvmSignatureDiagnostics.addAll(diagnostics.forElement(declaration));
|
||||
}
|
||||
return jvmSignatureDiagnostics;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return jetFile.getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,214 +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.checkers
|
||||
|
||||
import org.jetbrains.kotlin.descriptors.Named
|
||||
import org.jetbrains.kotlin.load.java.structure.JavaNamedElement
|
||||
import org.jetbrains.kotlin.load.java.structure.impl.JavaClassImpl
|
||||
import org.jetbrains.kotlin.load.java.structure.impl.JavaTypeImpl
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.name.Name
|
||||
import org.jetbrains.kotlin.renderer.DescriptorRenderer
|
||||
import org.jetbrains.kotlin.resolve.DescriptorUtils
|
||||
import org.jetbrains.kotlin.resolve.calls.tasks.ResolutionCandidate
|
||||
import org.jetbrains.kotlin.resolve.scopes.MemberScope
|
||||
import org.jetbrains.kotlin.serialization.ProtoBuf
|
||||
import org.jetbrains.kotlin.serialization.deserialization.DeserializationContext
|
||||
import org.jetbrains.kotlin.serialization.deserialization.TypeDeserializer
|
||||
import org.jetbrains.kotlin.types.KotlinType
|
||||
import org.jetbrains.kotlin.utils.Printer
|
||||
import java.lang.reflect.Constructor
|
||||
import java.lang.reflect.GenericDeclaration
|
||||
import java.lang.reflect.Method
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class LazyOperationsLog(
|
||||
val stringSanitizer: (String) -> String
|
||||
) {
|
||||
val ids = IdentityHashMap<Any, Int>()
|
||||
private fun objectId(o: Any): Int = ids.getOrPut(o, { ids.size })
|
||||
|
||||
private class Record(
|
||||
val lambda: Any,
|
||||
val data: LoggingStorageManager.CallData
|
||||
)
|
||||
|
||||
private val records = ArrayList<Record>()
|
||||
|
||||
val addRecordFunction: (lambda: Any, LoggingStorageManager.CallData) -> Unit = {
|
||||
lambda, data ->
|
||||
records.add(Record(lambda, data))
|
||||
}
|
||||
|
||||
fun getText(): String {
|
||||
val groupedByOwner = records.groupByTo(IdentityHashMap()) {
|
||||
it.data.fieldOwner
|
||||
}.map { Pair(it.key, it.value) }
|
||||
|
||||
return groupedByOwner.map {
|
||||
val (owner, records) = it
|
||||
renderOwner(owner, records)
|
||||
}.sortedBy(stringSanitizer).joinToString("\n").renumberObjects()
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces ids in the given string so that they increase
|
||||
* Example:
|
||||
* input = "A@21 B@6"
|
||||
* output = "A@0 B@1"
|
||||
*/
|
||||
private fun String.renumberObjects(): String {
|
||||
val ids = HashMap<String, String>()
|
||||
fun newId(objectId: String): String {
|
||||
return ids.getOrPut(objectId, { "@" + ids.size })
|
||||
}
|
||||
|
||||
val m = Pattern.compile("@\\d+").matcher(this)
|
||||
val sb = StringBuffer()
|
||||
while (m.find()) {
|
||||
m.appendReplacement(sb, newId(m.group(0)))
|
||||
}
|
||||
m.appendTail(sb)
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun renderOwner(owner: Any?, records: List<Record>): String {
|
||||
val sb = StringBuilder()
|
||||
with (Printer(sb)) {
|
||||
println(render(owner), " {")
|
||||
indent {
|
||||
records.map { renderRecord(it) }.sortedBy(stringSanitizer).forEach {
|
||||
println(it)
|
||||
}
|
||||
}
|
||||
println("}")
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun renderRecord(record: Record): String {
|
||||
val data = record.data
|
||||
val sb = StringBuilder()
|
||||
|
||||
sb.append(data.field?.name ?: "in ${data.lambdaCreatedIn.getDeclarationName()}")
|
||||
|
||||
if (!data.arguments.isEmpty()) {
|
||||
data.arguments.joinTo(sb, ", ", "(", ")") { render(it) }
|
||||
}
|
||||
|
||||
sb.append(" = ${render(data.result)}")
|
||||
|
||||
if (data.fieldOwner is MemberScope) {
|
||||
sb.append(" // through ${render(data.fieldOwner)}")
|
||||
}
|
||||
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun render(o: Any?): String {
|
||||
if (o == null) return "null"
|
||||
|
||||
val sb = StringBuilder()
|
||||
if (o is FqName || o is Name || o is String || o is Number || o is Boolean) {
|
||||
sb.append("'$o': ")
|
||||
}
|
||||
|
||||
val id = objectId(o)
|
||||
|
||||
val aClass = o.javaClass
|
||||
sb.append(if (aClass.isAnonymousClass) aClass.name.substringAfterLast('.') else aClass.simpleName).append("@$id")
|
||||
|
||||
fun Any.appendQuoted() {
|
||||
sb.append("['").append(this).append("']")
|
||||
}
|
||||
|
||||
when {
|
||||
o is Named -> o.name.appendQuoted()
|
||||
o.javaClass.simpleName == "LazyJavaClassifierType" -> {
|
||||
val javaType = o.field<JavaTypeImpl<*>>("javaType")
|
||||
javaType.psi.presentableText.appendQuoted()
|
||||
}
|
||||
o.javaClass.simpleName == "LazyJavaClassTypeConstructor" -> {
|
||||
val javaClass = o.field<Any>("this\$0").field<JavaClassImpl>("jClass")
|
||||
javaClass.psi.name!!.appendQuoted()
|
||||
}
|
||||
o.javaClass.simpleName == "DeserializedType" -> {
|
||||
val typeDeserializer = o.field<TypeDeserializer>("typeDeserializer")
|
||||
val context = typeDeserializer.field<DeserializationContext>("c")
|
||||
val typeProto = o.field<ProtoBuf.Type>("typeProto")
|
||||
val text = when {
|
||||
typeProto.hasClassName() -> context.nameResolver.getClassId(typeProto.className).asSingleFqName().asString()
|
||||
typeProto.hasTypeParameter() -> {
|
||||
val classifier = (o as KotlinType).constructor.declarationDescriptor!!
|
||||
"" + classifier.name + " in " + DescriptorUtils.getFqName(classifier.containingDeclaration)
|
||||
}
|
||||
else -> "???"
|
||||
}
|
||||
text.appendQuoted()
|
||||
}
|
||||
o is JavaNamedElement -> {
|
||||
o.name.appendQuoted()
|
||||
}
|
||||
o is JavaTypeImpl<*> -> {
|
||||
o.psi.presentableText.appendQuoted()
|
||||
}
|
||||
o is Collection<*> -> {
|
||||
if (o.isEmpty()) {
|
||||
sb.append("[empty]")
|
||||
}
|
||||
else {
|
||||
sb.append("[${o.size}] ")
|
||||
o.joinTo(sb, ", ", prefix = "{", postfix = "}", limit = 3) { render(it) }
|
||||
}
|
||||
}
|
||||
o is KotlinType -> {
|
||||
StringBuilder().apply {
|
||||
append(o.constructor)
|
||||
if (!o.arguments.isEmpty()) {
|
||||
append("<${o.arguments.size}>")
|
||||
}
|
||||
}.appendQuoted()
|
||||
}
|
||||
o is ResolutionCandidate<*> -> DescriptorRenderer.COMPACT.render(o.descriptor).appendQuoted()
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Any.field(name: String): T {
|
||||
val field = this.javaClass.getDeclaredField(name)
|
||||
field.isAccessible = true
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return field.get(this) as T
|
||||
}
|
||||
|
||||
private fun Printer.indent(body: Printer.() -> Unit): Printer {
|
||||
pushIndent()
|
||||
body()
|
||||
popIndent()
|
||||
return this
|
||||
}
|
||||
|
||||
private fun GenericDeclaration?.getDeclarationName(): String? {
|
||||
return when (this) {
|
||||
is Class<*> -> getName().substringAfterLast(".")
|
||||
is Method -> declaringClass.getDeclarationName() + "::" + name + "()"
|
||||
is Constructor<*> -> getDeclaringClass().getDeclarationName() + "::" + getName() + "()"
|
||||
else -> "<no name>"
|
||||
}
|
||||
}
|
||||
@@ -1,128 +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.checkers
|
||||
|
||||
import org.jetbrains.kotlin.storage.ObservableStorageManager
|
||||
import org.jetbrains.kotlin.storage.StorageManager
|
||||
import java.lang.reflect.Field
|
||||
import java.lang.reflect.GenericDeclaration
|
||||
|
||||
class LoggingStorageManager(
|
||||
private val delegate: StorageManager,
|
||||
private val callHandler: (lambda: Any, call: LoggingStorageManager.CallData?) -> Unit) : ObservableStorageManager(delegate) {
|
||||
|
||||
class CallData(
|
||||
val fieldOwner: Any?,
|
||||
val field: Field?,
|
||||
val lambdaCreatedIn: GenericDeclaration?,
|
||||
val arguments: List<Any?>,
|
||||
val result: Any?
|
||||
)
|
||||
|
||||
// Creating objects here because we need a reference to it
|
||||
override val <T> (() -> T).observable: () -> T
|
||||
get() = object : () -> T {
|
||||
override fun invoke(): T {
|
||||
val result = this@observable()
|
||||
callHandler(this@observable, computeCallerData(this@observable, this, listOf(), result))
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Creating objects here because we need a reference to it
|
||||
override val <K, V> ((K) -> V).observable: (K) -> V
|
||||
get() = object : (K) -> V {
|
||||
override fun invoke(p1: K): V {
|
||||
val result = this@observable(p1)
|
||||
callHandler(this@observable, computeCallerData(this@observable, this, listOf(p1), result))
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeCallerData(lambda: Any, wrapper: Any, arguments: List<Any?>, result: Any?): CallData {
|
||||
val lambdaClass = lambda.javaClass
|
||||
|
||||
val outerClass: Class<out Any?>? = lambdaClass.enclosingClass
|
||||
|
||||
// fields named "this" or "this$0"
|
||||
val referenceToOuter = lambdaClass.getAllDeclaredFields().firstOrNull {
|
||||
field ->
|
||||
field.type == outerClass && field.name!!.contains("this")
|
||||
}
|
||||
referenceToOuter?.isAccessible = true
|
||||
|
||||
val outerInstance = referenceToOuter?.get(lambda)
|
||||
|
||||
fun Class<*>.findFunctionField(): Field? {
|
||||
return this.getAllDeclaredFields().firstOrNull {
|
||||
it.type?.name?.startsWith("kotlin.Function") ?: false
|
||||
}
|
||||
}
|
||||
val containingField = if (outerInstance == null) null
|
||||
else outerClass?.getAllDeclaredFields()?.firstOrNull {
|
||||
field ->
|
||||
field.isAccessible = true
|
||||
val value = field.get(outerInstance)
|
||||
if (value == null) return@firstOrNull false
|
||||
|
||||
val valueClass = value.javaClass
|
||||
val functionField = valueClass.findFunctionField()
|
||||
if (functionField == null) return@firstOrNull false
|
||||
|
||||
functionField.isAccessible = true
|
||||
val functionValue = functionField.get(value)
|
||||
functionValue == wrapper
|
||||
}
|
||||
|
||||
if (containingField == null) {
|
||||
val wrappedLambdaField = lambdaClass.findFunctionField()
|
||||
if (wrappedLambdaField != null) {
|
||||
wrappedLambdaField.isAccessible = true
|
||||
val wrappedLambda = wrappedLambdaField.get(lambda)
|
||||
return CallData(outerInstance, null, enclosingEntity(wrappedLambda.javaClass), arguments, result)
|
||||
}
|
||||
}
|
||||
|
||||
val enclosingEntity = enclosingEntity(lambdaClass)
|
||||
|
||||
return CallData(outerInstance, containingField, enclosingEntity, arguments, result)
|
||||
}
|
||||
|
||||
private fun enclosingEntity(_class: Class<Any>): GenericDeclaration? {
|
||||
val result = _class.enclosingConstructor
|
||||
?: _class.enclosingMethod
|
||||
?: _class.enclosingClass
|
||||
|
||||
return result as GenericDeclaration?
|
||||
}
|
||||
|
||||
private fun Class<*>.getAllDeclaredFields(): List<Field> {
|
||||
val result = arrayListOf<Field>()
|
||||
|
||||
var c = this
|
||||
while (true) {
|
||||
result.addAll(c.declaredFields.toList())
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val superClass = (c as Class<Any>).superclass as Class<Any>?
|
||||
if (superClass == null) break
|
||||
if (c == superClass) break
|
||||
c = superClass
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user