caae6ff2ec
Forbid underscore-only (_, __, ___, ...) names as callees and as types. If CHECK_TYPE directive is on, filter out UNDERSCORE_USAGE_WITHOUT_BACKTICKS messages.
428 lines
20 KiB
Kotlin
428 lines
20 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.checkers.CheckerTestUtil.ActualDiagnostic
|
|
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.resolve.MultiTargetPlatform
|
|
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
|
|
}
|
|
|
|
data class DiagnosticTestLanguageVersionSettings(
|
|
private val languageFeatures: Map<LanguageFeature, LanguageFeature.State>,
|
|
override val apiVersion: ApiVersion,
|
|
override val languageVersion: LanguageVersion
|
|
) : LanguageVersionSettings {
|
|
private val delegate = LanguageVersionSettingsImpl(languageVersion, apiVersion)
|
|
|
|
override fun getFeatureSupport(feature: LanguageFeature): LanguageFeature.State =
|
|
languageFeatures[feature] ?: delegate.getFeatureSupport(feature)
|
|
|
|
override fun isFlagEnabled(flag: AnalysisFlag): Boolean = false
|
|
}
|
|
|
|
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.declareCheckType = CHECK_TYPE_DIRECTIVE in directives
|
|
this.whatDiagnosticsToConsider = parseDiagnosticFilterDirective(directives, declareCheckType)
|
|
this.customLanguageVersionSettings = parseLanguageVersionSettings(directives)
|
|
this.checkLazyLog = CHECK_LAZY_LOG_DIRECTIVE in directives || CHECK_LAZY_LOG_DEFAULT
|
|
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)
|
|
}
|
|
}
|
|
|
|
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,
|
|
implementingModulesBindings: List<Pair<MultiTargetPlatform, BindingContext>>,
|
|
actualText: StringBuilder,
|
|
skipJvmSignatureDiagnostics: Boolean
|
|
): Boolean {
|
|
if (this.ktFile == null) {
|
|
// TODO: check java files too
|
|
actualText.append(this.clearText)
|
|
return true
|
|
}
|
|
|
|
// TODO: report JVM signature diagnostics also for implementing modules
|
|
val jvmSignatureDiagnostics = if (skipJvmSignatureDiagnostics)
|
|
emptySet<ActualDiagnostic>()
|
|
else
|
|
computeJvmSignatureDiagnostics(bindingContext)
|
|
|
|
val ok = booleanArrayOf(true)
|
|
val diagnostics = ContainerUtil.filter(
|
|
CheckerTestUtil.getDiagnosticsIncludingSyntaxErrors(
|
|
bindingContext, implementingModulesBindings, ktFile, markDynamicCalls, dynamicCallDescriptors
|
|
) + jvmSignatureDiagnostics,
|
|
{ whatDiagnosticsToConsider.value(it.diagnostic) }
|
|
)
|
|
|
|
val diagnosticToExpectedDiagnostic = CheckerTestUtil.diagnosticsDiff(diagnosedRanges, diagnostics, object : CheckerTestUtil.DiagnosticDiffCallbacks {
|
|
override fun missingDiagnostic(diagnostic: CheckerTestUtil.TextDiagnostic, expectedStart: Int, expectedEnd: Int) {
|
|
val message = "Missing " + diagnostic.description + 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.description}${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<ActualDiagnostic> {
|
|
val jvmSignatureDiagnostics = HashSet<ActualDiagnostic>()
|
|
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).map { ActualDiagnostic(it, null) })
|
|
}
|
|
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"
|
|
val LANGUAGE_VERSION = "LANGUAGE_VERSION"
|
|
private val LANGUAGE_PATTERN = Pattern.compile("(\\+|\\-|warn:)(\\w+)\\s*")
|
|
|
|
val DEFAULT_DIAGNOSTIC_TESTS_FEATURES = mapOf(
|
|
LanguageFeature.Coroutines to LanguageFeature.State.ENABLED
|
|
)
|
|
|
|
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]
|
|
val languageVersionDirective = directiveMap[LANGUAGE_VERSION]
|
|
if (apiVersionString == null && directives == null && languageVersionDirective == 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()
|
|
|
|
val languageVersion: LanguageVersion = languageVersionDirective?.let { LanguageVersion.fromVersionString(it) } ?: LanguageVersion.LATEST
|
|
|
|
return DiagnosticTestLanguageVersionSettings(languageFeatures, apiVersion, languageVersion)
|
|
}
|
|
|
|
private fun collectLanguageFeatureMap(directives: String): Map<LanguageFeature, LanguageFeature.State> {
|
|
val matcher = LANGUAGE_PATTERN.matcher(directives)
|
|
if (!matcher.find()) {
|
|
Assert.fail(
|
|
"Wrong syntax in the '// !$LANGUAGE_DIRECTIVE: ...' directive:\n" +
|
|
"found: '$directives'\n" +
|
|
"Must be '((+|-|warn:)LanguageFeatureName)+'\n" +
|
|
"where '+' means 'enable', '-' means 'disable', 'warn:' means 'enable with warning'\n" +
|
|
"and language feature names are names of enum entries in LanguageFeature enum class"
|
|
)
|
|
}
|
|
|
|
val values = HashMap<LanguageFeature, LanguageFeature.State>()
|
|
do {
|
|
val mode = when (matcher.group(1)) {
|
|
"+" -> LanguageFeature.State.ENABLED
|
|
"-" -> LanguageFeature.State.DISABLED
|
|
"warn:" -> LanguageFeature.State.ENABLED_WITH_WARNING
|
|
else -> error("Unknown mode for language feature: ${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, mode) != null) {
|
|
Assert.fail("Duplicate entry for the language feature: $name")
|
|
}
|
|
}
|
|
while (matcher.find())
|
|
|
|
return values
|
|
}
|
|
|
|
private fun parseDiagnosticFilterDirective(directiveMap: Map<String, String>, allowUnderscoreUsage: Boolean): Condition<Diagnostic> {
|
|
val directives = directiveMap[DIAGNOSTICS_DIRECTIVE]
|
|
val initialCondition =
|
|
if (allowUnderscoreUsage)
|
|
Condition<Diagnostic> { it.factory.name != "UNDERSCORE_USAGE_WITHOUT_BACKTICKS" }
|
|
else
|
|
Conditions.alwaysTrue()
|
|
|
|
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 Conditions.and(initialCondition, Condition {
|
|
diagnostic -> diagnostic.factory !== Errors.NEWER_VERSION_IN_SINCE_KOTLIN
|
|
})
|
|
}
|
|
return initialCondition
|
|
}
|
|
|
|
var condition = initialCondition
|
|
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 }
|
|
)
|
|
}
|
|
}
|
|
}
|