738a84e259
Add a file in package 'kotlin.jvm.functions' to the project and replace
explicit imports of functions 'kotlin.Function{n}' to two imports 'kotlin.*'
and 'kotlin.jvm.functions.*', so that the codebase can be compiled both by the
old and by the new compiler
413 lines
18 KiB
Java
413 lines
18 KiB
Java
/*
|
|
* 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 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 kotlin.jvm.functions.*;
|
|
import kotlin.*;
|
|
import kotlin.KotlinPackage;
|
|
import org.jetbrains.annotations.NotNull;
|
|
import org.jetbrains.annotations.Nullable;
|
|
import org.jetbrains.kotlin.asJava.AsJavaPackage;
|
|
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor;
|
|
import org.jetbrains.kotlin.diagnostics.*;
|
|
import org.jetbrains.kotlin.psi.JetDeclaration;
|
|
import org.jetbrains.kotlin.psi.JetFile;
|
|
import org.jetbrains.kotlin.resolve.BindingContext;
|
|
import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics;
|
|
import org.jetbrains.kotlin.types.Flexibility;
|
|
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
|
|
KotlinMultiFileTestWithWithJava<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 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>._() {}" +
|
|
"\nfun <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 = Flexibility.FLEXIBLE_TYPE_CLASSIFIER.getPackageFqName().asString();
|
|
public static final String EXPLICIT_FLEXIBLE_CLASS_NAME = Flexibility.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 = KotlinPackage.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<JetFile> getJetFiles(List<? extends TestFile> testFiles, boolean includeExtras) {
|
|
boolean declareFlexibleType = false;
|
|
boolean declareCheckType = false;
|
|
List<JetFile> 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(createPsiFile(null, "EXPLICIT_FLEXIBLE_TYPES.kt", EXPLICIT_FLEXIBLE_TYPES_DECLARATIONS));
|
|
}
|
|
if (declareCheckType) {
|
|
jetFiles.add(createPsiFile(null, "CHECK_TYPE.kt", CHECK_TYPE_DECLARATIONS));
|
|
}
|
|
}
|
|
|
|
return jetFiles;
|
|
}
|
|
|
|
public static Condition<Diagnostic> parseDiagnosticFilterDirective(Map<String, String> directiveMap) {
|
|
String directives = directiveMap.get(DIAGNOSTICS_DIRECTIVE);
|
|
if (directives == null) {
|
|
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:\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());
|
|
}
|
|
}
|
|
|
|
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 JetFile jetFile;
|
|
private final Condition<Diagnostic> whatDiagnosticsToConsider;
|
|
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.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 = createCheckAndReturnPsiFile(null, fileName, clearText);
|
|
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 JetFile 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(
|
|
KotlinPackage.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<JetDeclaration> declarations = PsiTreeUtil.findChildrenOfType(jetFile, JetDeclaration.class);
|
|
for (JetDeclaration declaration : declarations) {
|
|
Diagnostics diagnostics = AsJavaPackage.getJvmSignatureDiagnostics(declaration, bindingContext.getDiagnostics(),
|
|
GlobalSearchScope.allScope(getProject()));
|
|
if (diagnostics == null) continue;
|
|
jvmSignatureDiagnostics.addAll(diagnostics.forElement(declaration));
|
|
}
|
|
return jvmSignatureDiagnostics;
|
|
}
|
|
}
|
|
}
|