/* * 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.KotlinPackage; import kotlin.jvm.functions.Function1; 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 { public static final String DIAGNOSTICS_DIRECTIVE = "DIAGNOSTICS"; public static final Pattern DIAGNOSTICS_PATTERN = Pattern.compile("([\\+\\-!])(\\w+)\\s*"); public static final ImmutableSet> 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 checkSubtype(t: T) = t" + "\nclass Inv" + "\nfun Inv._() {}" + "\ninfix fun T.checkType(f: Inv.() -> 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 + ""; 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 directives) { return new TestFile(module, fileName, text, directives); } @Override protected void doMultiFileTest(File file, final Map modules, List testFiles) { for (final ModuleAndDependencies moduleAndDependencies : modules.values()) { List dependencies = KotlinPackage.map( moduleAndDependencies.dependencies, new Function1() { @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 files ); protected List getJetFiles(List testFiles, boolean includeExtras) { boolean declareFlexibleType = false; boolean declareCheckType = false; List 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 parseDiagnosticFilterDirective(Map directiveMap) { String directives = directiveMap.get(DIAGNOSTICS_DIRECTIVE); if (directives == null) { return Conditions.alwaysTrue(); } Condition 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 newCondition; if (ImmutableSet.of("ERROR", "WARNING", "INFO").contains(name)) { final Severity severity = Severity.valueOf(name); newCondition = new Condition() { @Override public boolean value(Diagnostic diagnostic) { return diagnostic.getSeverity() == severity; } }; } else { newCondition = new Condition() { @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() { @Override public boolean value(Diagnostic diagnostic) { return DIAGNOSTICS_TO_INCLUDE_ANYWAY.contains(diagnostic.getFactory()); } }); } protected static class TestModule implements Comparable { private final String name; private final List dependencies = new ArrayList(); public TestModule(@NotNull String name) { this.name = name; } @NotNull public String getName() { return name; } @NotNull public List getDependencies() { return dependencies; } @Override public int compareTo(@NotNull TestModule module) { return name.compareTo(module.getName()); } } protected class TestFile { private final List diagnosedRanges = Lists.newArrayList(); public final String expectedText; private final TestModule module; private final String clearText; private final JetFile jetFile; private final Condition whatDiagnosticsToConsider; private final boolean declareCheckType; private final boolean declareFlexibleType; public final boolean checkLazyLog; private final boolean markDynamicCalls; private final List dynamicCallDescriptors = new ArrayList(); public TestFile( @Nullable TestModule module, String fileName, String textWithMarkers, Map 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 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 jvmSignatureDiagnostics = skipJvmSignatureDiagnostics ? Collections.emptySet() : computeJvmSignatureDiagnostics(bindingContext); final boolean[] ok = { true }; List diagnostics = ContainerUtil.filter( KotlinPackage.plus(CheckerTestUtil.getDiagnosticsIncludingSyntaxErrors(bindingContext, jetFile, markDynamicCalls, dynamicCallDescriptors), jvmSignatureDiagnostics), whatDiagnosticsToConsider ); Map 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() { @Override public String fun(PsiFile file) { return file.getText(); } })); stripExtras(actualText); return ok[0]; } private Set computeJvmSignatureDiagnostics(BindingContext bindingContext) { Set jvmSignatureDiagnostics = new HashSet(); Collection 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; } } }