Files
kotlin-fork/compiler/tests-common/org/jetbrains/kotlin/checkers/BaseDiagnosticsTest.kt
T
2016-12-23 22:30:06 +03:00

408 lines
19 KiB
Kotlin

/*
* 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.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.PsiFileFactory
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.containers.ContainerUtil
import org.jetbrains.kotlin.asJava.getJvmSignatureDiagnostics
import org.jetbrains.kotlin.checkers.BaseDiagnosticsTest.TestFile
import org.jetbrains.kotlin.checkers.BaseDiagnosticsTest.TestModule
import org.jetbrains.kotlin.config.*
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.test.KotlinTestUtils
import org.jetbrains.kotlin.utils.addIfNotNull
import org.junit.Assert
import java.io.File
import java.util.*
import java.util.regex.Pattern
abstract class BaseDiagnosticsTest : KotlinMultiFileTestWithJava<TestModule, TestFile>() {
override fun createTestModule(name: String): TestModule =
TestModule(name)
override fun createTestFile(module: TestModule?, fileName: String, text: String, directives: Map<String, String>): TestFile =
TestFile(module, fileName, text, directives)
override fun doMultiFileTest(
file: File,
modules: @JvmSuppressWildcards Map<String, ModuleAndDependencies>,
testFiles: List<TestFile>
) {
for (moduleAndDependencies in modules.values) {
moduleAndDependencies.module.getDependencies().addAll(moduleAndDependencies.dependencies.map { name ->
modules[name]?.module ?: error("Dependency not found: $name for module ${moduleAndDependencies.module.name}")
})
}
analyzeAndCheck(file, testFiles)
}
protected abstract fun analyzeAndCheck(testDataFile: File, files: List<TestFile>)
protected fun getKtFiles(testFiles: List<TestFile>, includeExtras: Boolean): List<KtFile> {
var declareFlexibleType = false
var declareCheckType = false
val ktFiles = arrayListOf<KtFile>()
for (testFile in testFiles) {
ktFiles.addIfNotNull(testFile.ktFile)
declareFlexibleType = declareFlexibleType or testFile.declareFlexibleType
declareCheckType = declareCheckType or testFile.declareCheckType
}
if (includeExtras) {
if (declareFlexibleType) {
ktFiles.add(KotlinTestUtils.createFile("EXPLICIT_FLEXIBLE_TYPES.kt", EXPLICIT_FLEXIBLE_TYPES_DECLARATIONS, project))
}
if (declareCheckType) {
ktFiles.add(KotlinTestUtils.createFile("CHECK_TYPE.kt", CHECK_TYPE_DECLARATIONS, project))
}
}
return ktFiles
}
class TestModule(val name: String) : Comparable<TestModule> {
private val dependencies = ArrayList<TestModule>()
fun getDependencies(): MutableList<TestModule> = dependencies
override fun compareTo(other: TestModule): Int = name.compareTo(other.name)
override fun toString(): String = name
}
class DiagnosticTestLanguageVersionSettings(
private val languageFeatures: Map<LanguageFeature, Boolean>,
override val apiVersion: ApiVersion
) : LanguageVersionSettings {
override fun supportsFeature(feature: LanguageFeature): Boolean =
languageFeatures[feature] ?: LanguageVersionSettingsImpl.DEFAULT.supportsFeature(feature)
// TODO provide base language version
override val languageVersion: LanguageVersion
get() = throw UnsupportedOperationException("This instance of LanguageVersionSettings should be used for tests only")
override fun equals(other: Any?): Boolean =
other is DiagnosticTestLanguageVersionSettings && other.languageFeatures == languageFeatures && other.apiVersion == apiVersion
override fun hashCode(): Int =
31 * languageFeatures.hashCode() + apiVersion.hashCode()
}
inner class TestFile(
val module: TestModule?,
fileName: String,
textWithMarkers: String,
directives: Map<String, String>
) {
private val diagnosedRanges: List<CheckerTestUtil.DiagnosedRange> = ArrayList()
val expectedText: String
private val clearText: String
val ktFile: KtFile?
private val whatDiagnosticsToConsider: Condition<Diagnostic>
val customLanguageVersionSettings: LanguageVersionSettings?
val declareCheckType: Boolean
val declareFlexibleType: Boolean
val checkLazyLog: Boolean
private val markDynamicCalls: Boolean
val dynamicCallDescriptors: List<DeclarationDescriptor> = ArrayList()
init {
this.whatDiagnosticsToConsider = parseDiagnosticFilterDirective(directives)
this.customLanguageVersionSettings = parseLanguageVersionSettings(directives)
this.checkLazyLog = CHECK_LAZY_LOG_DIRECTIVE in directives || CHECK_LAZY_LOG_DEFAULT
this.declareCheckType = CHECK_TYPE_DIRECTIVE in directives
this.declareFlexibleType = EXPLICIT_FLEXIBLE_TYPES_DIRECTIVE in directives
this.markDynamicCalls = MARK_DYNAMIC_CALLS_DIRECTIVE in directives
if (fileName.endsWith(".java")) {
PsiFileFactory.getInstance(project).createFileFromText(fileName, JavaLanguage.INSTANCE, textWithMarkers)
// TODO: check there's not syntax errors
this.ktFile = null
this.clearText = textWithMarkers
this.expectedText = this.clearText
}
else {
this.expectedText = textWithMarkers
this.clearText = CheckerTestUtil.parseDiagnosedRanges(addExtras(expectedText), diagnosedRanges)
this.ktFile = TestCheckerUtil.createCheckAndReturnPsiFile(fileName, clearText, project)
for (diagnosedRange in diagnosedRanges) {
diagnosedRange.file = ktFile
}
}
}
private val imports: String
get() = buildString {
// Line separator is "\n" intentionally here (see DocumentImpl.assertValidSeparators)
if (declareCheckType) {
append(CHECK_TYPE_IMPORT + "\n")
}
if (declareFlexibleType) {
append(EXPLICIT_FLEXIBLE_TYPES_IMPORT + "\n")
}
}
private val extras: String
get() = "/*extras*/\n$imports/*extras*/\n\n"
private fun addExtras(text: String): String =
addImports(text, extras)
private fun stripExtras(actualText: StringBuilder) {
val extras = extras
val start = actualText.indexOf(extras)
if (start >= 0) {
actualText.delete(start, start + extras.length)
}
}
private fun addImports(text: String, imports: String): String {
var result = text
val pattern = Pattern.compile("^package [\\.\\w\\d]*\n", Pattern.MULTILINE)
val matcher = pattern.matcher(result)
if (matcher.find()) {
// add imports after the package directive
result = result.substring(0, matcher.end()) + imports + result.substring(matcher.end())
}
else {
// add imports at the beginning
result = imports + result
}
return result
}
fun getActualText(bindingContext: BindingContext, actualText: StringBuilder, skipJvmSignatureDiagnostics: Boolean): Boolean {
if (this.ktFile == null) {
// TODO: check java files too
actualText.append(this.clearText)
return true
}
val jvmSignatureDiagnostics = if (skipJvmSignatureDiagnostics)
emptySet<Diagnostic>()
else
computeJvmSignatureDiagnostics(bindingContext)
val ok = booleanArrayOf(true)
val diagnostics = ContainerUtil.filter(
CheckerTestUtil.getDiagnosticsIncludingSyntaxErrors(
bindingContext, ktFile, markDynamicCalls, dynamicCallDescriptors
) + jvmSignatureDiagnostics,
whatDiagnosticsToConsider
)
val diagnosticToExpectedDiagnostic = hashMapOf<Diagnostic, CheckerTestUtil.TextDiagnostic>()
CheckerTestUtil.diagnosticsDiff(diagnosticToExpectedDiagnostic, diagnosedRanges, diagnostics, object : CheckerTestUtil.DiagnosticDiffCallbacks {
override fun missingDiagnostic(diagnostic: CheckerTestUtil.TextDiagnostic, expectedStart: Int, expectedEnd: Int) {
val message = "Missing " + diagnostic.name + DiagnosticUtils.atLocation(ktFile, TextRange(expectedStart, expectedEnd))
System.err.println(message)
ok[0] = false
}
override fun wrongParametersDiagnostic(
expectedDiagnostic: CheckerTestUtil.TextDiagnostic,
actualDiagnostic: CheckerTestUtil.TextDiagnostic,
start: Int,
end: Int
) {
val message = "Parameters of diagnostic not equal at position " +
DiagnosticUtils.atLocation(ktFile, TextRange(start, end)) +
". Expected: ${expectedDiagnostic.asString()}, actual: $actualDiagnostic"
System.err.println(message)
ok[0] = false
}
override fun unexpectedDiagnostic(diagnostic: CheckerTestUtil.TextDiagnostic, actualStart: Int, actualEnd: Int) {
val message = "Unexpected ${diagnostic.name}${DiagnosticUtils.atLocation(ktFile, TextRange(actualStart, actualEnd))}"
System.err.println(message)
ok[0] = false
}
})
actualText.append(
CheckerTestUtil.addDiagnosticMarkersToText(ktFile, diagnostics, diagnosticToExpectedDiagnostic, { file -> file.text })
)
stripExtras(actualText)
return ok[0]
}
private fun computeJvmSignatureDiagnostics(bindingContext: BindingContext): Set<Diagnostic> {
val jvmSignatureDiagnostics = HashSet<Diagnostic>()
val declarations = PsiTreeUtil.findChildrenOfType(ktFile, KtDeclaration::class.java)
for (declaration in declarations) {
val diagnostics = getJvmSignatureDiagnostics(declaration, bindingContext.diagnostics,
GlobalSearchScope.allScope(project)) ?: continue
jvmSignatureDiagnostics.addAll(diagnostics.forElement(declaration))
}
return jvmSignatureDiagnostics
}
override fun toString(): String = ktFile!!.name
}
companion object {
val DIAGNOSTICS_DIRECTIVE = "DIAGNOSTICS"
val DIAGNOSTICS_PATTERN: Pattern = Pattern.compile("([\\+\\-!])(\\w+)\\s*")
val DIAGNOSTICS_TO_INCLUDE_ANYWAY: Set<DiagnosticFactory<*>> = setOf(
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
)
val LANGUAGE_DIRECTIVE = "LANGUAGE"
private val LANGUAGE_PATTERN = Pattern.compile("([\\+\\-])(\\w+)\\s*")
val API_VERSION_DIRECTIVE = "API_VERSION"
val CHECK_TYPE_DIRECTIVE = "CHECK_TYPE"
val CHECK_TYPE_PACKAGE = "tests._checkType"
private val 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) {}"
val CHECK_TYPE_IMPORT = "import $CHECK_TYPE_PACKAGE.*"
val EXPLICIT_FLEXIBLE_TYPES_DIRECTIVE = "EXPLICIT_FLEXIBLE_TYPES"
val EXPLICIT_FLEXIBLE_PACKAGE = InternalFlexibleTypeTransformer.FLEXIBLE_TYPE_CLASSIFIER.packageFqName.asString()
val EXPLICIT_FLEXIBLE_CLASS_NAME = InternalFlexibleTypeTransformer.FLEXIBLE_TYPE_CLASSIFIER.relativeClassName.asString()
private val EXPLICIT_FLEXIBLE_TYPES_DECLARATIONS = "\npackage " + EXPLICIT_FLEXIBLE_PACKAGE +
"\npublic class " + EXPLICIT_FLEXIBLE_CLASS_NAME + "<L, U>"
private val EXPLICIT_FLEXIBLE_TYPES_IMPORT = "import $EXPLICIT_FLEXIBLE_PACKAGE.$EXPLICIT_FLEXIBLE_CLASS_NAME"
val CHECK_LAZY_LOG_DIRECTIVE = "CHECK_LAZY_LOG"
val CHECK_LAZY_LOG_DEFAULT = "true" == System.getProperty("check.lazy.logs", "false")
val MARK_DYNAMIC_CALLS_DIRECTIVE = "MARK_DYNAMIC_CALLS"
private fun parseLanguageVersionSettings(directiveMap: Map<String, String>): LanguageVersionSettings? {
val apiVersionString = directiveMap[API_VERSION_DIRECTIVE]
val directives = directiveMap[LANGUAGE_DIRECTIVE]
if (apiVersionString == null && directives == null) return null
val apiVersion = (if (apiVersionString != null) ApiVersion.parse(apiVersionString) else ApiVersion.LATEST)
?: error("Unknown API version: $apiVersionString")
val languageFeatures = directives?.let(this::collectLanguageFeatureMap).orEmpty()
return DiagnosticTestLanguageVersionSettings(languageFeatures, apiVersion)
}
private fun collectLanguageFeatureMap(directives: String): Map<LanguageFeature, Boolean> {
val 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"
)
}
val values = HashMap<LanguageFeature, Boolean>()
do {
val enable = matcher.group(1) == "+"
val name = matcher.group(2)
val feature = LanguageFeature.fromString(name) ?: throw AssertionError(
"Language feature not found, please check spelling: $name\n" +
"Known features:\n ${LanguageFeature.values().joinToString("\n ")}"
)
if (values.put(feature, enable) != null) {
Assert.fail("Duplicate entry for the language feature: $name")
}
}
while (matcher.find())
return values
}
private fun parseDiagnosticFilterDirective(directiveMap: Map<String, String>): Condition<Diagnostic> {
val directives = directiveMap[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 (API_VERSION_DIRECTIVE in directiveMap) {
return Condition { diagnostic -> diagnostic.factory !== Errors.NEWER_VERSION_IN_SINCE_KOTLIN }
}
return Conditions.alwaysTrue()
}
var condition = Conditions.alwaysTrue<Diagnostic>()
val 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")
}
var first = true
do {
val operation = matcher.group(1)
val name = matcher.group(2)
val newCondition: Condition<Diagnostic> =
if (name in setOf("ERROR", "WARNING", "INFO")) {
Condition { diagnostic -> diagnostic.severity == Severity.valueOf(name) }
}
else {
Condition { diagnostic -> name == diagnostic.factory.name }
}
when (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
}
"+" -> condition = Conditions.or(condition, newCondition)
"-" -> 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,
Condition { diagnostic -> diagnostic.factory in DIAGNOSTICS_TO_INCLUDE_ANYWAY }
)
}
}
}