Quick-fix for header without implementation + a set of tests #KT-14908 Fixed
This commit is contained in:
@@ -176,6 +176,10 @@ class KtPsiFactory(private val project: Project) {
|
||||
return createDeclaration(text)
|
||||
}
|
||||
|
||||
fun createObject(text: String): KtObjectDeclaration {
|
||||
return createDeclaration(text)
|
||||
}
|
||||
|
||||
fun createCompanionObject(): KtObjectDeclaration {
|
||||
return createClass("class A {\n companion object{\n}\n}").getCompanionObjects().first()
|
||||
}
|
||||
|
||||
@@ -115,7 +115,9 @@ private val MODIFIERS_TO_REPLACE = mapOf(
|
||||
PUBLIC_KEYWORD to listOf(PROTECTED_KEYWORD, PRIVATE_KEYWORD, INTERNAL_KEYWORD),
|
||||
PROTECTED_KEYWORD to listOf(PUBLIC_KEYWORD, PRIVATE_KEYWORD, INTERNAL_KEYWORD),
|
||||
PRIVATE_KEYWORD to listOf(PUBLIC_KEYWORD, PROTECTED_KEYWORD, INTERNAL_KEYWORD),
|
||||
INTERNAL_KEYWORD to listOf(PUBLIC_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD)
|
||||
INTERNAL_KEYWORD to listOf(PUBLIC_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD),
|
||||
HEADER_KEYWORD to listOf(IMPL_KEYWORD),
|
||||
IMPL_KEYWORD to listOf(HEADER_KEYWORD)
|
||||
)
|
||||
|
||||
private val MODIFIERS_ORDER = listOf(PUBLIC_KEYWORD, PROTECTED_KEYWORD, PRIVATE_KEYWORD, INTERNAL_KEYWORD,
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createTypeParameter.Cr
|
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateLocalVariableActionFactory
|
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateParameterByNamedArgumentActionFactory
|
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateParameterByRefActionFactory
|
||||
import org.jetbrains.kotlin.idea.quickfix.createImpl.CreateHeaderImplementationFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.migration.MigrateExternalExtensionFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.migration.MigrateTypeParameterListFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.replaceWith.DeprecatedSymbolUsageFix
|
||||
@@ -461,6 +462,8 @@ class QuickFixRegistrar : QuickFixContributor {
|
||||
OVERLOADS_LOCAL.registerFactory(RemoveAnnotationFix.JvmOverloads)
|
||||
OVERLOADS_WITHOUT_DEFAULT_ARGUMENTS.registerFactory(RemoveAnnotationFix.JvmOverloads)
|
||||
|
||||
HEADER_WITHOUT_IMPLEMENTATION.registerFactory(CreateHeaderImplementationFix)
|
||||
|
||||
ErrorsJs.WRONG_EXTERNAL_DECLARATION.registerFactory(MigrateExternalExtensionFix)
|
||||
}
|
||||
}
|
||||
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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.idea.quickfix.createImpl
|
||||
|
||||
import com.intellij.codeInsight.intention.IntentionAction
|
||||
import com.intellij.ide.util.PackageUtil
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.ModuleUtilCore
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.roots.ModuleRootManager
|
||||
import com.intellij.psi.JavaDirectoryService
|
||||
import com.intellij.psi.codeStyle.CodeStyleManager
|
||||
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
|
||||
import org.jetbrains.kotlin.descriptors.*
|
||||
import org.jetbrains.kotlin.diagnostics.Diagnostic
|
||||
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory
|
||||
import org.jetbrains.kotlin.diagnostics.Errors
|
||||
import org.jetbrains.kotlin.idea.core.ShortenReferences
|
||||
import org.jetbrains.kotlin.idea.core.TemplateKind
|
||||
import org.jetbrains.kotlin.idea.core.getFunctionBodyTextFromTemplate
|
||||
import org.jetbrains.kotlin.idea.core.toDescriptor
|
||||
import org.jetbrains.kotlin.idea.project.TargetPlatformDetector
|
||||
import org.jetbrains.kotlin.idea.quickfix.KotlinQuickFixAction
|
||||
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory
|
||||
import org.jetbrains.kotlin.idea.refactoring.getOrCreateKotlinFile
|
||||
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
|
||||
import org.jetbrains.kotlin.idea.util.projectStructure.allModules
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
|
||||
sealed class CreateHeaderImplementationFix<out D : KtNamedDeclaration>(
|
||||
declaration: D,
|
||||
private val implPlatformKind: PlatformKind,
|
||||
private val generateIt: KtPsiFactory.(Project, D) -> D?
|
||||
) : KotlinQuickFixAction<D>(declaration) {
|
||||
|
||||
override fun getFamilyName() = text
|
||||
|
||||
protected abstract val elementType: String
|
||||
|
||||
override fun getText() = "Create header $elementType implementation for platform ${implPlatformKind.name}"
|
||||
|
||||
override final fun invoke(project: Project, editor: Editor?, file: KtFile) {
|
||||
val element = element ?: return
|
||||
val factory = KtPsiFactory(project)
|
||||
val implFile = getOrCreateImplementationFile(project) ?: return
|
||||
|
||||
val implDeclaration = implFile.add(factory.generateIt(project, element) ?: return) as KtElement
|
||||
val reformatted = CodeStyleManager.getInstance(project).reformat(implDeclaration)
|
||||
ShortenReferences.DEFAULT.process(reformatted as KtElement)
|
||||
}
|
||||
|
||||
private fun Project.implementationModuleOf(headerModule: Module) =
|
||||
allModules().firstOrNull {
|
||||
TargetPlatformDetector.getPlatform(it).kind == implPlatformKind &&
|
||||
headerModule in ModuleRootManager.getInstance(it).dependencies
|
||||
}
|
||||
|
||||
private fun getOrCreateImplementationFile(
|
||||
project: Project
|
||||
): KtFile? {
|
||||
val declaration = element as? KtNamedDeclaration ?: return null
|
||||
val name = declaration.name ?: return null
|
||||
|
||||
val headerDir = declaration.containingFile.containingDirectory
|
||||
val headerPackage = JavaDirectoryService.getInstance().getPackage(headerDir)
|
||||
|
||||
val headerModule = ModuleUtilCore.findModuleForPsiElement(declaration) ?: return null
|
||||
val implModule = project.implementationModuleOf(headerModule) ?: return null
|
||||
val implDirectory = PackageUtil.findOrCreateDirectoryForPackage(
|
||||
implModule, headerPackage?.qualifiedName ?: "", null, false
|
||||
) ?: return null
|
||||
return getOrCreateKotlinFile("$name.kt", implDirectory)
|
||||
}
|
||||
|
||||
companion object : KotlinSingleIntentionActionFactory() {
|
||||
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
|
||||
val d = DiagnosticFactory.cast(diagnostic, Errors.HEADER_WITHOUT_IMPLEMENTATION)
|
||||
val declaration = d.psiElement as? KtNamedDeclaration ?: return null
|
||||
val compatibility = d.c
|
||||
if (compatibility.isNotEmpty()) return null
|
||||
val implPlatformKind = d.b.platformKind
|
||||
return when (declaration) {
|
||||
is KtClassOrObject -> CreateHeaderClassImplementationFix(declaration, implPlatformKind)
|
||||
is KtFunction -> CreateHeaderFunctionImplementationFix(declaration, implPlatformKind)
|
||||
is KtProperty -> CreateHeaderPropertyImplementationFix(declaration, implPlatformKind)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CreateHeaderClassImplementationFix(
|
||||
klass: KtClassOrObject,
|
||||
implPlatformKind: PlatformKind
|
||||
) : CreateHeaderImplementationFix<KtClassOrObject>(klass, implPlatformKind, { project, element ->
|
||||
generateClassOrObject(project, element, implNeeded = true)
|
||||
}) {
|
||||
|
||||
override val elementType = if ((element as? KtClass)?.isInterface() ?: false) "interface" else "class"
|
||||
}
|
||||
|
||||
class CreateHeaderPropertyImplementationFix(
|
||||
property: KtProperty,
|
||||
implPlatformKind: PlatformKind
|
||||
) : CreateHeaderImplementationFix<KtProperty>(property, implPlatformKind, { project, element ->
|
||||
val descriptor = element.toDescriptor() as? PropertyDescriptor
|
||||
descriptor?.let { generateProperty(project, element, descriptor, implNeeded = true) }
|
||||
}) {
|
||||
|
||||
override val elementType = "property"
|
||||
}
|
||||
|
||||
class CreateHeaderFunctionImplementationFix(
|
||||
function: KtFunction,
|
||||
implPlatformKind: PlatformKind
|
||||
) : CreateHeaderImplementationFix<KtFunction>(function, implPlatformKind, { project, element ->
|
||||
val descriptor = element.toDescriptor() as? FunctionDescriptor
|
||||
descriptor?.let { generateFunction(project, element, descriptor, implNeeded = true) }
|
||||
}) {
|
||||
|
||||
override val elementType = "function"
|
||||
}
|
||||
|
||||
private fun KtModifierListOwner.replaceHeaderModifier(implNeeded: Boolean) {
|
||||
if (implNeeded) {
|
||||
addModifier(KtTokens.IMPL_KEYWORD)
|
||||
}
|
||||
else {
|
||||
removeModifier(KtTokens.HEADER_KEYWORD)
|
||||
}
|
||||
}
|
||||
|
||||
private fun KtPsiFactory.generateClassOrObject(
|
||||
project: Project,
|
||||
headerClass: KtClassOrObject,
|
||||
implNeeded: Boolean
|
||||
): KtClassOrObject {
|
||||
val header = headerClass.text
|
||||
val implClass = if (headerClass is KtObjectDeclaration) createObject(header) else createClass(header)
|
||||
if (headerClass !is KtClass || !headerClass.isInterface()) {
|
||||
implClass.declarations.forEach {
|
||||
if (it !is KtEnumEntry &&
|
||||
it !is KtClassOrObject &&
|
||||
!it.hasModifier(KtTokens.ABSTRACT_KEYWORD)) {
|
||||
it.delete()
|
||||
}
|
||||
}
|
||||
|
||||
declLoop@ for (headerDeclaration in headerClass.declarations) {
|
||||
if (headerDeclaration.hasModifier(KtTokens.ABSTRACT_KEYWORD)) continue
|
||||
val descriptor = headerDeclaration.toDescriptor() ?: continue
|
||||
val implDeclaration: KtDeclaration = when (headerDeclaration) {
|
||||
is KtFunction -> generateFunction(project, headerDeclaration, descriptor as FunctionDescriptor, implNeeded = true)
|
||||
is KtProperty -> generateProperty(project, headerDeclaration, descriptor as PropertyDescriptor, implNeeded = true)
|
||||
else -> continue@declLoop
|
||||
}
|
||||
implClass.addDeclaration(implDeclaration)
|
||||
}
|
||||
}
|
||||
|
||||
return implClass.apply {
|
||||
replaceHeaderModifier(implNeeded)
|
||||
}
|
||||
}
|
||||
|
||||
private fun KtPsiFactory.generateFunction(
|
||||
project: Project,
|
||||
headerFunction: KtFunction,
|
||||
descriptor: FunctionDescriptor,
|
||||
implNeeded: Boolean
|
||||
): KtFunction {
|
||||
val returnType = descriptor.returnType
|
||||
val body = run {
|
||||
if (returnType != null && !KotlinBuiltIns.isUnit(returnType)) {
|
||||
val delegation = getFunctionBodyTextFromTemplate(
|
||||
project,
|
||||
TemplateKind.FUNCTION,
|
||||
descriptor.name.asString(),
|
||||
IdeDescriptorRenderers.SOURCE_CODE.renderType(returnType)
|
||||
)
|
||||
|
||||
"{$delegation\n}"
|
||||
}
|
||||
else {
|
||||
"{}"
|
||||
}
|
||||
}
|
||||
|
||||
return if (headerFunction is KtSecondaryConstructor) {
|
||||
createSecondaryConstructor(headerFunction.text + " " + body)
|
||||
}
|
||||
else {
|
||||
createFunction(headerFunction.text + " " + body).apply {
|
||||
replaceHeaderModifier(implNeeded)
|
||||
if (returnType != null && KotlinBuiltIns.isUnit(returnType)) {
|
||||
typeReference = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun KtPsiFactory.generateProperty(
|
||||
project: Project,
|
||||
headerProperty: KtProperty,
|
||||
descriptor: PropertyDescriptor,
|
||||
implNeeded: Boolean
|
||||
): KtProperty {
|
||||
val body = buildString {
|
||||
append("\nget()")
|
||||
append(" = ")
|
||||
append(getFunctionBodyTextFromTemplate(
|
||||
project,
|
||||
TemplateKind.FUNCTION,
|
||||
descriptor.name.asString(),
|
||||
descriptor.returnType?.let { IdeDescriptorRenderers.SOURCE_CODE.renderType(it) } ?: ""
|
||||
))
|
||||
if (descriptor.isVar) {
|
||||
append("\nset(value) {}")
|
||||
}
|
||||
}
|
||||
return createProperty(headerProperty.text + " " + body).apply {
|
||||
replaceHeaderModifier(implNeeded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// "Create header class implementation for platform JS" "true"
|
||||
|
||||
header abstract class <caret>Abstract {
|
||||
fun foo(param: String): Int
|
||||
|
||||
abstract fun String.bar(y: Double): Boolean
|
||||
|
||||
val isGood: Boolean
|
||||
|
||||
abstract var status: Int
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// "Create header class implementation for platform JS" "true"
|
||||
|
||||
header abstract class Abstract {
|
||||
fun foo(param: String): Int
|
||||
|
||||
abstract fun String.bar(y: Double): Boolean
|
||||
|
||||
val isGood: Boolean
|
||||
|
||||
abstract var status: Int
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// Abstract: to be implemented
|
||||
@@ -0,0 +1,13 @@
|
||||
// Abstract: to be implemented
|
||||
impl abstract class Abstract {
|
||||
|
||||
abstract fun String.bar(y: Double): Boolean
|
||||
|
||||
abstract var status: Int
|
||||
impl fun foo(param: String): Int {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
impl val isGood: Boolean
|
||||
get() = TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// "Create header class implementation for platform JVM" "true"
|
||||
|
||||
header class <caret>My {
|
||||
fun foo(param: String): Int
|
||||
|
||||
fun String.bar(y: Double): Boolean
|
||||
|
||||
fun baz(): Unit
|
||||
|
||||
constructor(flag: Boolean)
|
||||
|
||||
val isGood: Boolean
|
||||
|
||||
var status: Int
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// "Create header class implementation for platform JVM" "true"
|
||||
|
||||
header class My {
|
||||
fun foo(param: String): Int
|
||||
|
||||
fun String.bar(y: Double): Boolean
|
||||
|
||||
fun baz(): Unit
|
||||
|
||||
constructor(flag: Boolean)
|
||||
|
||||
val isGood: Boolean
|
||||
|
||||
var status: Int
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// My: to be implemented
|
||||
@@ -0,0 +1,23 @@
|
||||
// My: to be implemented
|
||||
impl class My {
|
||||
impl fun foo(param: String): Int {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
impl fun String.bar(y: Double): Boolean {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
impl fun baz() {}
|
||||
|
||||
constructor(flag: Boolean) {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
impl val isGood: Boolean
|
||||
get() = TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
impl var status: Int
|
||||
get() = TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
set(value) {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// "Create header class implementation for platform JS" "true"
|
||||
|
||||
header enum class <caret>MyEnum {
|
||||
FIRST,
|
||||
SECOND,
|
||||
LAST;
|
||||
|
||||
val num: Int
|
||||
|
||||
companion object {
|
||||
fun byNum(num: Int): MyEnum = when (num) {
|
||||
1 -> FIRST
|
||||
2 -> SECOND
|
||||
else -> LAST
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// "Create header class implementation for platform JS" "true"
|
||||
|
||||
header enum class MyEnum {
|
||||
FIRST,
|
||||
SECOND,
|
||||
LAST;
|
||||
|
||||
val num: Int
|
||||
|
||||
companion object {
|
||||
fun byNum(num: Int): MyEnum = when (num) {
|
||||
1 -> FIRST
|
||||
2 -> SECOND
|
||||
else -> LAST
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// MyEnum: to be implemented
|
||||
@@ -0,0 +1,17 @@
|
||||
// MyEnum: to be implemented
|
||||
impl enum class MyEnum {
|
||||
FIRST,
|
||||
SECOND,
|
||||
LAST;
|
||||
|
||||
companion object {
|
||||
fun byNum(num: Int): MyEnum = when (num) {
|
||||
1 -> FIRST
|
||||
2 -> SECOND
|
||||
else -> LAST
|
||||
}
|
||||
}
|
||||
|
||||
impl val num: Int
|
||||
get() = TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// "Create header function implementation for platform JVM" "true"
|
||||
|
||||
header fun <caret>foo(arg: Int): String
|
||||
@@ -0,0 +1,3 @@
|
||||
// "Create header function implementation for platform JVM" "true"
|
||||
|
||||
header fun foo(arg: Int): String
|
||||
@@ -0,0 +1 @@
|
||||
// foo: to be implemented
|
||||
@@ -0,0 +1,4 @@
|
||||
// foo: to be implemented
|
||||
impl fun foo(arg: Int): String {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// "Create header interface implementation for platform JVM" "true"
|
||||
|
||||
header interface <caret>Interface {
|
||||
fun foo(param: String): Int
|
||||
|
||||
fun String.bar(y: Double): Boolean
|
||||
|
||||
val isGood: Boolean
|
||||
|
||||
var status: Int
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// "Create header interface implementation for platform JVM" "true"
|
||||
|
||||
header interface Interface {
|
||||
fun foo(param: String): Int
|
||||
|
||||
fun String.bar(y: Double): Boolean
|
||||
|
||||
val isGood: Boolean
|
||||
|
||||
var status: Int
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// Interface: to be implemented
|
||||
@@ -0,0 +1,10 @@
|
||||
// Interface: to be implemented
|
||||
impl interface Interface {
|
||||
fun foo(param: String): Int
|
||||
|
||||
fun String.bar(y: Double): Boolean
|
||||
|
||||
val isGood: Boolean
|
||||
|
||||
var status: Int
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// "Create header class implementation for platform JVM" "true"
|
||||
|
||||
header class <caret>WithNested {
|
||||
fun foo(): Int
|
||||
|
||||
class Nested {
|
||||
fun bar() = "Nested"
|
||||
}
|
||||
|
||||
inner class Inner {
|
||||
fun baz() = "Inner"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// "Create header class implementation for platform JVM" "true"
|
||||
|
||||
header class WithNested {
|
||||
fun foo(): Int
|
||||
|
||||
class Nested {
|
||||
fun bar() = "Nested"
|
||||
}
|
||||
|
||||
inner class Inner {
|
||||
fun baz() = "Inner"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// WithNested: to be implemented
|
||||
@@ -0,0 +1,15 @@
|
||||
// WithNested: to be implemented
|
||||
impl class WithNested {
|
||||
|
||||
class Nested {
|
||||
fun bar() = "Nested"
|
||||
}
|
||||
|
||||
inner class Inner {
|
||||
fun baz() = "Inner"
|
||||
}
|
||||
|
||||
impl fun foo(): Int {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// "Create header class implementation for platform JVM" "true"
|
||||
|
||||
header object <caret>Object {
|
||||
fun foo(): String
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// "Create header class implementation for platform JVM" "true"
|
||||
|
||||
header object Object {
|
||||
fun foo(): String
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// Object: to be implemented
|
||||
@@ -0,0 +1,6 @@
|
||||
// Object: to be implemented
|
||||
impl object Object {
|
||||
impl fun foo(): String {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// "Create header property implementation for platform JVM" "true"
|
||||
|
||||
header var <caret>x: Int
|
||||
@@ -0,0 +1,3 @@
|
||||
// "Create header property implementation for platform JVM" "true"
|
||||
|
||||
header var x: Int
|
||||
@@ -0,0 +1 @@
|
||||
// x: to be implemented
|
||||
@@ -0,0 +1,4 @@
|
||||
// x: to be implemented
|
||||
impl var x: Int
|
||||
get() = TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
set(value) {}
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Create header class implementation for platform JS" "true"
|
||||
|
||||
header sealed class <caret>Sealed {
|
||||
object Obj : Sealed()
|
||||
|
||||
class Klass(val x: Int) : Sealed()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Create header class implementation for platform JS" "true"
|
||||
|
||||
header sealed class Sealed {
|
||||
object Obj : Sealed()
|
||||
|
||||
class Klass(val x: Int) : Sealed()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// Sealed: to be implemented
|
||||
@@ -0,0 +1,6 @@
|
||||
// Sealed: to be implemented
|
||||
impl sealed class Sealed {
|
||||
object Obj : Sealed()
|
||||
|
||||
class Klass(val x: Int) : Sealed()
|
||||
}
|
||||
+1
-84
@@ -16,32 +16,14 @@
|
||||
|
||||
package org.jetbrains.kotlin.idea.caches.resolve
|
||||
|
||||
import com.intellij.openapi.application.WriteAction
|
||||
import com.intellij.openapi.command.WriteCommandAction
|
||||
import com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProviderImpl
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.StdModuleTypes
|
||||
import com.intellij.openapi.roots.DependencyScope
|
||||
import com.intellij.openapi.roots.ModuleRootModificationUtil
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.testFramework.PsiTestUtil
|
||||
import com.sampullara.cli.Argument
|
||||
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
|
||||
import org.jetbrains.kotlin.config.CompilerSettings
|
||||
import org.jetbrains.kotlin.config.KotlinFacetSettingsProvider
|
||||
import org.jetbrains.kotlin.config.TargetPlatformKind
|
||||
import org.jetbrains.kotlin.idea.facet.getOrCreateFacet
|
||||
import org.jetbrains.kotlin.idea.project.PluginJetFilesProvider
|
||||
import org.jetbrains.kotlin.idea.stubs.AbstractMultiHighlightingTest
|
||||
import org.jetbrains.kotlin.idea.test.ConfigLibraryUtil
|
||||
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
|
||||
import org.junit.Assert
|
||||
import java.io.File
|
||||
|
||||
abstract class AbstractMultiModuleHighlightingTest : AbstractMultiHighlightingTest() {
|
||||
|
||||
protected open val testPath = PluginTestCaseBase.getTestDataPathBase() + "/multiModuleHighlighting/"
|
||||
override val testPath = PluginTestCaseBase.getTestDataPathBase() + "/multiModuleHighlighting/"
|
||||
|
||||
protected fun checkHighlightingInAllFiles() {
|
||||
var atLeastOneFile = false
|
||||
@@ -54,69 +36,4 @@ abstract class AbstractMultiModuleHighlightingTest : AbstractMultiHighlightingTe
|
||||
}
|
||||
Assert.assertTrue(atLeastOneFile)
|
||||
}
|
||||
|
||||
protected fun module(name: String, hasTestRoot: Boolean = false, useFullJdk: Boolean = false): Module {
|
||||
val srcDir = testPath + "${getTestName(true)}/$name"
|
||||
val moduleWithSrcRootSet = createModuleFromTestData(srcDir, name, StdModuleTypes.JAVA, true)!!
|
||||
if (hasTestRoot) {
|
||||
setTestRoot(moduleWithSrcRootSet, name)
|
||||
}
|
||||
|
||||
val jdkToUse = if (useFullJdk) PluginTestCaseBase.fullJdk() else PluginTestCaseBase.mockJdk()
|
||||
ConfigLibraryUtil.configureSdk(moduleWithSrcRootSet, jdkToUse)
|
||||
|
||||
return moduleWithSrcRootSet
|
||||
}
|
||||
|
||||
protected fun setTestRoot(module: Module, name: String) {
|
||||
val testDir = testPath + "${getTestName(true)}/${name}Test"
|
||||
val testRootDirInTestData = File(testDir)
|
||||
val testRootDir = createTempDirectory()!!
|
||||
FileUtil.copyDir(testRootDirInTestData, testRootDir)
|
||||
val testRoot = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(testRootDir)!!
|
||||
object : WriteCommandAction.Simple<Unit>(project) {
|
||||
override fun run() {
|
||||
testRoot.refresh(false, true)
|
||||
}
|
||||
}.execute().throwException()
|
||||
PsiTestUtil.addSourceRoot(module, testRoot, true)
|
||||
}
|
||||
|
||||
protected fun Module.addDependency(
|
||||
other: Module,
|
||||
dependencyScope: DependencyScope = DependencyScope.COMPILE,
|
||||
exported: Boolean = false
|
||||
) = ModuleRootModificationUtil.addDependency(this, other, dependencyScope, exported)
|
||||
|
||||
private fun Module.createFacet() {
|
||||
val accessToken = WriteAction.start()
|
||||
try {
|
||||
val modelsProvider = IdeModifiableModelsProviderImpl(project)
|
||||
getOrCreateFacet(modelsProvider)
|
||||
modelsProvider.commit()
|
||||
}
|
||||
finally {
|
||||
accessToken.finish()
|
||||
}
|
||||
}
|
||||
|
||||
protected fun Module.setPlatformKind(platformKind: TargetPlatformKind<*>) {
|
||||
createFacet()
|
||||
val facetSettings = KotlinFacetSettingsProvider.getInstance(project).getSettings(this)
|
||||
val versionInfo = facetSettings.versionInfo
|
||||
versionInfo.targetPlatformKind = platformKind
|
||||
}
|
||||
|
||||
protected fun Module.enableMultiPlatform() {
|
||||
createFacet()
|
||||
val facetSettings = KotlinFacetSettingsProvider.getInstance(project).getSettings(this)
|
||||
val compilerInfo = facetSettings.compilerInfo
|
||||
val compilerSettings = CompilerSettings()
|
||||
compilerSettings.additionalArguments += " -$multiPlatformArg"
|
||||
compilerInfo.compilerSettings = compilerSettings
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val multiPlatformArg = CommonCompilerArguments::multiPlatform.annotations.filterIsInstance<Argument>().single().value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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.idea.quickfix
|
||||
|
||||
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixTestCase
|
||||
import com.intellij.codeInsight.intention.IntentionAction
|
||||
import com.intellij.codeInsight.intention.impl.ShowIntentionActionsHandler
|
||||
import com.intellij.openapi.command.CommandProcessor
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import junit.framework.ComparisonFailure
|
||||
import junit.framework.TestCase
|
||||
import org.jetbrains.kotlin.idea.inspections.findExistingEditor
|
||||
import org.jetbrains.kotlin.idea.project.PluginJetFilesProvider
|
||||
import org.jetbrains.kotlin.idea.stubs.AbstractMultiModuleTest
|
||||
import org.jetbrains.kotlin.idea.test.DirectiveBasedActionUtils
|
||||
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
|
||||
import org.jetbrains.kotlin.psi.KtFile
|
||||
import org.jetbrains.kotlin.test.KotlinTestUtils
|
||||
import java.io.File
|
||||
import java.util.regex.Pattern
|
||||
|
||||
abstract class AbstractQuickFixMultiModuleTest : AbstractMultiModuleTest() {
|
||||
|
||||
override val testPath = PluginTestCaseBase.getTestDataPathBase() + "/multiModuleQuickFix/"
|
||||
|
||||
override fun getTestDataPath() = testPath
|
||||
|
||||
protected fun shouldBeAvailableAfterExecution() = false
|
||||
|
||||
private fun getActionsTexts(availableActions: List<IntentionAction>) = availableActions.map { it.text }
|
||||
|
||||
protected fun doQuickFixTest() {
|
||||
val allFilesInProject = PluginJetFilesProvider.allFilesInProject(myProject!!)
|
||||
val actionFile = allFilesInProject.single { file ->
|
||||
file.text.contains("// \"")
|
||||
}
|
||||
|
||||
configureByExistingFile(actionFile.virtualFile!!)
|
||||
|
||||
val actionFileText = actionFile.text
|
||||
val actionFileName = actionFile.name
|
||||
|
||||
CommandProcessor.getInstance().executeCommand(project, {
|
||||
try {
|
||||
val psiFile = actionFile
|
||||
|
||||
val pair = LightQuickFixTestCase.parseActionHint(psiFile, actionFileText)
|
||||
val text = pair.getFirst()
|
||||
|
||||
val actionShouldBeAvailable = pair.getSecond()
|
||||
|
||||
if (psiFile is KtFile) {
|
||||
DirectiveBasedActionUtils.checkForUnexpectedErrors(psiFile)
|
||||
}
|
||||
|
||||
doAction(text, actionShouldBeAvailable, actionFileName)
|
||||
|
||||
if (actionShouldBeAvailable) {
|
||||
val testDirectory = File(testPath)
|
||||
val projectDirectory = File("$testPath${getTestName(true)}")
|
||||
for (moduleDirectory in projectDirectory.listFiles()) {
|
||||
for (file in moduleDirectory.listFiles()) {
|
||||
if (!file.path.endsWith(".after")) continue
|
||||
try {
|
||||
val editedFile = allFilesInProject.find {
|
||||
it.name.toLowerCase() == file.name.removeSuffix(".after").toLowerCase()
|
||||
}!!
|
||||
setActiveEditor(editedFile.findExistingEditor() ?: createEditor(editedFile.virtualFile))
|
||||
checkResultByFile(file.relativeTo(testDirectory).path)
|
||||
}
|
||||
catch (e: ComparisonFailure) {
|
||||
KotlinTestUtils.assertEqualsToFile(file, editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e: ComparisonFailure) {
|
||||
throw e
|
||||
}
|
||||
catch (e: AssertionError) {
|
||||
throw e
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
TestCase.fail(getTestName(true))
|
||||
}
|
||||
}, "", "")
|
||||
}
|
||||
|
||||
// TODO: merge with AbstractQuickFixMultiFileTest
|
||||
|
||||
fun doAction(text: String, actionShouldBeAvailable: Boolean, testFilePath: String) {
|
||||
val pattern = if (text.startsWith("/"))
|
||||
Pattern.compile(text.substring(1, text.length - 1))
|
||||
else
|
||||
Pattern.compile(StringUtil.escapeToRegexp(text))
|
||||
|
||||
val availableActions = getAvailableActions()
|
||||
val action = findActionByPattern(pattern, availableActions)
|
||||
|
||||
if (action == null) {
|
||||
if (actionShouldBeAvailable) {
|
||||
val texts = getActionsTexts(availableActions)
|
||||
val infos = doHighlighting()
|
||||
TestCase.fail("Action with text '" + text + "' is not available in test " + testFilePath + "\n" +
|
||||
"Available actions (" + texts.size + "): \n" +
|
||||
StringUtil.join(texts, "\n") +
|
||||
"\nActions:\n" +
|
||||
StringUtil.join(availableActions, "\n") +
|
||||
"\nInfos:\n" +
|
||||
StringUtil.join(infos, "\n"))
|
||||
}
|
||||
else {
|
||||
DirectiveBasedActionUtils.checkAvailableActionsAreExpected(file, availableActions)
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!actionShouldBeAvailable) {
|
||||
TestCase.fail("Action '$text' is available (but must not) in test $testFilePath")
|
||||
}
|
||||
|
||||
ShowIntentionActionsHandler.chooseActionAndInvoke(file, editor, action, action.text)
|
||||
|
||||
UIUtil.dispatchAllInvocationEvents()
|
||||
|
||||
|
||||
if (!shouldBeAvailableAfterExecution()) {
|
||||
val afterAction = findActionByPattern(pattern, getAvailableActions())
|
||||
|
||||
if (afterAction != null) {
|
||||
TestCase.fail("Action '$text' is still available after its invocation in test $testFilePath")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findActionByPattern(pattern: Pattern, availableActions: List<IntentionAction>): IntentionAction? {
|
||||
for (availableAction in availableActions) {
|
||||
if (pattern.matcher(availableAction.text).matches()) {
|
||||
return availableAction
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getAvailableActions(): List<IntentionAction> {
|
||||
doHighlighting()
|
||||
return LightQuickFixTestCase.getAvailableActions(editor, file)
|
||||
}
|
||||
|
||||
override fun getTestProjectJdk() = PluginTestCaseBase.mockJdk()
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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.idea.quickfix
|
||||
|
||||
import org.jetbrains.kotlin.config.TargetPlatformKind
|
||||
import org.junit.Test
|
||||
|
||||
class QuickFixMultiModuleTest : AbstractQuickFixMultiModuleTest() {
|
||||
|
||||
private fun doMultiPlatformTest(headerName: String = "header",
|
||||
implName: String = "jvm",
|
||||
implKind: TargetPlatformKind<*> = TargetPlatformKind.Jvm.JVM_1_6) {
|
||||
val header = module(headerName)
|
||||
header.setPlatformKind(TargetPlatformKind.Common)
|
||||
|
||||
val jvm = module(implName)
|
||||
jvm.setPlatformKind(implKind)
|
||||
jvm.enableMultiPlatform()
|
||||
jvm.addDependency(header)
|
||||
|
||||
doQuickFixTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAbstract() {
|
||||
doMultiPlatformTest(implName = "js", implKind = TargetPlatformKind.JavaScript)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testClass() {
|
||||
doMultiPlatformTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEnum() {
|
||||
doMultiPlatformTest(implName = "js", implKind = TargetPlatformKind.JavaScript)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFunction() {
|
||||
doMultiPlatformTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInterface() {
|
||||
doMultiPlatformTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testObject() {
|
||||
doMultiPlatformTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNested() {
|
||||
doMultiPlatformTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testProperty() {
|
||||
doMultiPlatformTest()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSealed() {
|
||||
doMultiPlatformTest(implName = "js", implKind = TargetPlatformKind.JavaScript)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.jetbrains.kotlin.idea.stubs
|
||||
|
||||
import com.intellij.codeInsight.completion.CompletionTestCase
|
||||
import com.intellij.codeInsight.daemon.DaemonAnalyzerTestCase
|
||||
import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl
|
||||
import com.intellij.codeInsight.daemon.impl.HighlightInfo
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
@@ -29,7 +30,7 @@ import com.intellij.psi.search.GlobalSearchScope
|
||||
import com.intellij.psi.search.UsageSearchContext
|
||||
import com.intellij.testFramework.ExpectedHighlightingData
|
||||
|
||||
abstract class AbstractMultiHighlightingTest : CompletionTestCase() {
|
||||
abstract class AbstractMultiHighlightingTest : AbstractMultiModuleTest() {
|
||||
|
||||
protected open val shouldCheckLineMarkers = false
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.idea.stubs
|
||||
|
||||
import com.intellij.codeInsight.daemon.DaemonAnalyzerTestCase
|
||||
import com.intellij.openapi.application.WriteAction
|
||||
import com.intellij.openapi.command.WriteCommandAction
|
||||
import com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProviderImpl
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.module.StdModuleTypes
|
||||
import com.intellij.openapi.roots.DependencyScope
|
||||
import com.intellij.openapi.roots.ModuleRootModificationUtil
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.testFramework.PsiTestUtil
|
||||
import com.sampullara.cli.Argument
|
||||
import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments
|
||||
import org.jetbrains.kotlin.config.CompilerSettings
|
||||
import org.jetbrains.kotlin.config.KotlinFacetSettingsProvider
|
||||
import org.jetbrains.kotlin.config.TargetPlatformKind
|
||||
import org.jetbrains.kotlin.idea.facet.getOrCreateFacet
|
||||
import org.jetbrains.kotlin.idea.test.ConfigLibraryUtil
|
||||
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
|
||||
import java.io.File
|
||||
|
||||
abstract class AbstractMultiModuleTest : DaemonAnalyzerTestCase() {
|
||||
|
||||
protected open val testPath = PluginTestCaseBase.getTestDataPathBase()
|
||||
|
||||
protected fun module(name: String, hasTestRoot: Boolean = false, useFullJdk: Boolean = false): Module {
|
||||
val srcDir = testPath + "${getTestName(true)}/$name"
|
||||
val moduleWithSrcRootSet = createModuleFromTestData(srcDir, name, StdModuleTypes.JAVA, true)!!
|
||||
if (hasTestRoot) {
|
||||
setTestRoot(moduleWithSrcRootSet, name)
|
||||
}
|
||||
|
||||
val jdkToUse = if (useFullJdk) PluginTestCaseBase.fullJdk() else PluginTestCaseBase.mockJdk()
|
||||
ConfigLibraryUtil.configureSdk(moduleWithSrcRootSet, jdkToUse)
|
||||
|
||||
return moduleWithSrcRootSet
|
||||
}
|
||||
|
||||
private fun setTestRoot(module: Module, name: String) {
|
||||
val testDir = testPath + "${getTestName(true)}/${name}Test"
|
||||
val testRootDirInTestData = File(testDir)
|
||||
val testRootDir = createTempDirectory()!!
|
||||
FileUtil.copyDir(testRootDirInTestData, testRootDir)
|
||||
val testRoot = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(testRootDir)!!
|
||||
object : WriteCommandAction.Simple<Unit>(project) {
|
||||
override fun run() {
|
||||
testRoot.refresh(false, true)
|
||||
}
|
||||
}.execute().throwException()
|
||||
PsiTestUtil.addSourceRoot(module, testRoot, true)
|
||||
}
|
||||
|
||||
protected fun Module.addDependency(
|
||||
other: Module,
|
||||
dependencyScope: DependencyScope = DependencyScope.COMPILE,
|
||||
exported: Boolean = false
|
||||
) = ModuleRootModificationUtil.addDependency(this, other, dependencyScope, exported)
|
||||
|
||||
private fun Module.createFacet() {
|
||||
val accessToken = WriteAction.start()
|
||||
try {
|
||||
val modelsProvider = IdeModifiableModelsProviderImpl(project)
|
||||
getOrCreateFacet(modelsProvider)
|
||||
modelsProvider.commit()
|
||||
}
|
||||
finally {
|
||||
accessToken.finish()
|
||||
}
|
||||
}
|
||||
|
||||
protected fun Module.setPlatformKind(platformKind: TargetPlatformKind<*>) {
|
||||
createFacet()
|
||||
val facetSettings = KotlinFacetSettingsProvider.getInstance(project).getSettings(this)
|
||||
val versionInfo = facetSettings.versionInfo
|
||||
versionInfo.targetPlatformKind = platformKind
|
||||
}
|
||||
|
||||
protected fun Module.enableMultiPlatform() {
|
||||
createFacet()
|
||||
val facetSettings = KotlinFacetSettingsProvider.getInstance(project).getSettings(this)
|
||||
val compilerInfo = facetSettings.compilerInfo
|
||||
val compilerSettings = CompilerSettings()
|
||||
compilerSettings.additionalArguments += " -$multiPlatformArg"
|
||||
compilerInfo.compilerSettings = compilerSettings
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val multiPlatformArg = CommonCompilerArguments::multiPlatform.annotations.filterIsInstance<Argument>().single().value
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user