mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[kotlin] k2: quick doc on actual declaration retrieved from expect declaration
- test framework to set up light multiplatform tests in one file (KTIJ-28551) ^KTIJ-27961 fixed GitOrigin-RevId: 8b997886588a64069ccbc35c517fdfddb4ddb246
This commit is contained in:
committed by
intellij-monorepo-bot
parent
48d5ff6111
commit
7e87a9ace0
+48
@@ -0,0 +1,48 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.kotlin.idea.base.test
|
||||
|
||||
import java.io.File
|
||||
|
||||
data class TestFile(val relativePath: String, val text: String, val isMain: Boolean)
|
||||
|
||||
object ModuleStructureSplitter {
|
||||
private const val PLATFORM_PREFIX = "// PLATFORM:"
|
||||
private const val FILE_PATH_PREFIX = "// FILE:"
|
||||
private const val IS_MAIN = "// MAIN"
|
||||
|
||||
fun splitPerModule(file: File): MutableMap<String, MutableList<TestFile>> {
|
||||
val lines = file.readLines()
|
||||
return splitPerModule(lines)
|
||||
}
|
||||
|
||||
fun splitPerModule(lines: List<String>): MutableMap<String, MutableList<TestFile>> {
|
||||
val result = mutableMapOf<String, MutableList<TestFile>>()
|
||||
var currentPlatform: String? = null
|
||||
var currentFileName: String? = null
|
||||
var currentText: String? = null
|
||||
var isMainFile = false
|
||||
|
||||
fun dumpFile(currentPlatform: String?, currentFileName: String?, currentText: String?) {
|
||||
if (currentText != null && currentFileName != null && currentPlatform != null) {
|
||||
result.getOrPut(currentPlatform) { mutableListOf() }.add(TestFile(currentFileName, currentText, isMainFile))
|
||||
}
|
||||
}
|
||||
|
||||
lines.forEach { line ->
|
||||
if (line == IS_MAIN) {
|
||||
isMainFile = true
|
||||
} else if (line.startsWith(PLATFORM_PREFIX)) {
|
||||
dumpFile(currentPlatform, currentFileName, currentText)
|
||||
currentPlatform = line.substringAfter(PLATFORM_PREFIX).trim()
|
||||
} else if (line.startsWith(FILE_PATH_PREFIX)) {
|
||||
isMainFile = false
|
||||
currentFileName = line.substringAfter(FILE_PATH_PREFIX).trim()
|
||||
currentText = ""
|
||||
} else {
|
||||
currentText += line + "\n"
|
||||
}
|
||||
}
|
||||
dumpFile(currentPlatform, currentFileName, currentText)
|
||||
return result
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -32,6 +32,7 @@ import org.jetbrains.kotlin.idea.kdoc.KDocRenderer.createHighlightingManager
|
||||
import org.jetbrains.kotlin.idea.kdoc.KDocRenderer.generateJavadoc
|
||||
import org.jetbrains.kotlin.idea.kdoc.KDocRenderer.renderKDoc
|
||||
import org.jetbrains.kotlin.idea.references.mainReference
|
||||
import org.jetbrains.kotlin.idea.search.ExpectActualSupport
|
||||
import org.jetbrains.kotlin.kdoc.psi.api.KDoc
|
||||
import org.jetbrains.kotlin.kdoc.psi.impl.KDocSection
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
@@ -304,7 +305,18 @@ private fun findKDoc(symbol: KtSymbol): KDocContent? {
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
||||
if (symbol is KtValueParameterSymbol) {
|
||||
val containingSymbol = symbol.getContainingSymbol() as? KtFunctionSymbol
|
||||
if (containingSymbol != null) {
|
||||
val idx = containingSymbol.valueParameters.indexOf(symbol)
|
||||
containingSymbol.getExpectsForActual().filterIsInstance<KtFunctionSymbol>().mapNotNull { expectFunction ->
|
||||
findKDoc(expectFunction.valueParameters[idx])
|
||||
}.firstOrNull()?.let { return it }
|
||||
}
|
||||
}
|
||||
|
||||
return (symbol as? KtDeclarationSymbol)?.getExpectsForActual()?.mapNotNull { declarationSymbol -> findKDoc(declarationSymbol) }?.firstOrNull()
|
||||
}
|
||||
|
||||
context(KtAnalysisSession)
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.kotlin.idea.fir.documentation
|
||||
|
||||
import com.intellij.lang.documentation.ide.IdeDocumentationTargetProvider
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import com.intellij.openapi.vfs.VfsUtilCore
|
||||
import com.intellij.platform.backend.documentation.impl.computeDocumentationBlocking
|
||||
import com.intellij.platform.testFramework.core.FileComparisonFailedError
|
||||
import org.jetbrains.kotlin.idea.base.test.InTextDirectivesUtils
|
||||
import org.jetbrains.kotlin.idea.editor.quickDoc.AbstractQuickDocProviderTest.wrapToFileComparisonFailure
|
||||
import org.jetbrains.kotlin.idea.test.KotlinLightMultiplatformCodeInsightFixtureTestCase
|
||||
import java.io.File
|
||||
|
||||
abstract class AbstractFirQuickDocMultiplatformTest: KotlinLightMultiplatformCodeInsightFixtureTestCase() {
|
||||
override fun isFirPlugin(): Boolean = true
|
||||
|
||||
fun getDoc(): String? {
|
||||
val target =
|
||||
IdeDocumentationTargetProvider.getInstance(project).documentationTargets(editor, file, editor.caretModel.offset).firstOrNull()
|
||||
?: return null
|
||||
return computeDocumentationBlocking(target.createPointer())?.html
|
||||
}
|
||||
|
||||
fun doTest(path: String) {
|
||||
val virtualFile = configureModuleStructure(path)
|
||||
require(virtualFile != null)
|
||||
|
||||
myFixture.configureFromExistingVirtualFile(virtualFile)
|
||||
val element = myFixture.getFile().findElementAt(myFixture.editor.caretModel.offset)
|
||||
assertNotNull("Can't find element at caret in file: $path", element)
|
||||
var info = getDoc()
|
||||
if (info != null) {
|
||||
info = StringUtil.convertLineSeparators(info)
|
||||
}
|
||||
|
||||
val testDataFile = File(path)
|
||||
val textData = FileUtil.loadFile(testDataFile, true)
|
||||
val directives =
|
||||
InTextDirectivesUtils.findLinesWithPrefixesRemoved(textData, false, true, "INFO:")
|
||||
if (directives.isEmpty()) {
|
||||
throw FileComparisonFailedError(
|
||||
"'// INFO:' directive was expected",
|
||||
textData,
|
||||
"$textData\n\n//INFO: $info",
|
||||
testDataFile.absolutePath
|
||||
)
|
||||
}
|
||||
else {
|
||||
val expectedInfo = directives.joinToString("\n")
|
||||
if (expectedInfo != info) {
|
||||
wrapToFileComparisonFailure(info, path, textData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
package org.jetbrains.kotlin.idea.fir.documentation;
|
||||
|
||||
import com.intellij.testFramework.TestDataPath;
|
||||
import org.jetbrains.kotlin.idea.test.JUnit3RunnerWithInners;
|
||||
import org.jetbrains.kotlin.idea.test.KotlinTestUtils;
|
||||
import org.jetbrains.kotlin.test.TestMetadata;
|
||||
import org.jetbrains.kotlin.idea.base.test.TestRoot;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* This class is generated by {@link org.jetbrains.kotlin.testGenerator.generator.TestGenerator}.
|
||||
* DO NOT MODIFY MANUALLY.
|
||||
*/
|
||||
@SuppressWarnings("all")
|
||||
@TestRoot("fir/tests")
|
||||
@TestDataPath("$CONTENT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
@TestMetadata("../../idea/tests/testData/editor/quickDoc/multiplatform")
|
||||
public class FirQuickDocMultiplatformTestGenerated extends AbstractFirQuickDocMultiplatformTest {
|
||||
private void runTest(String testDataFilePath) throws Exception {
|
||||
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
|
||||
}
|
||||
|
||||
@TestMetadata("OnActualClassWithConstructorFromExpect.kt")
|
||||
public void testOnActualClassWithConstructorFromExpect() throws Exception {
|
||||
runTest("../../idea/tests/testData/editor/quickDoc/multiplatform/OnActualClassWithConstructorFromExpect.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("OnActualDeclarationWithInheritedDoc.kt")
|
||||
public void testOnActualDeclarationWithInheritedDoc() throws Exception {
|
||||
runTest("../../idea/tests/testData/editor/quickDoc/multiplatform/OnActualDeclarationWithInheritedDoc.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("OnActualValueParameterDeclaration.kt")
|
||||
public void testOnActualValueParameterDeclaration() throws Exception {
|
||||
runTest("../../idea/tests/testData/editor/quickDoc/multiplatform/OnActualValueParameterDeclaration.kt");
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// PLATFORM: Common
|
||||
// FILE: Foo.kt
|
||||
/**
|
||||
* @constructor Common Description
|
||||
*/
|
||||
expect class Foo()
|
||||
|
||||
// PLATFORM: Jvm
|
||||
// FILE: Foo.kt
|
||||
// MAIN
|
||||
actual class Foo actual co<caret>nstructor()
|
||||
|
||||
|
||||
|
||||
//INFO: <div class='definition'><pre><span style="color:#000080;font-weight:bold;">public</span> <span style="color:#000080;font-weight:bold;">actual</span> <span style="color:#000080;font-weight:bold;">constructor</span> <span style="color:#000000;">Foo</span>()</pre></div><div class='content'><p style='margin-top:0;padding-top:0;'>Common Description</p></div><table class='sections'></table><div class='bottom'><icon src="/org/jetbrains/kotlin/idea/icons/classKotlin.svg"/> <a href="psi_element://Foo"><code><span style="color:#000000;">Foo</span></code></a><br/></div>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// PLATFORM: Common
|
||||
// FILE: Foo.kt
|
||||
/**
|
||||
* Doc for expected class Foo
|
||||
*/
|
||||
expect class Foo
|
||||
|
||||
// PLATFORM: Jvm
|
||||
// FILE: Foo.kt
|
||||
// MAIN
|
||||
actual class F<caret>oo
|
||||
|
||||
//INFO: <div class='definition'><pre><span style="color:#000080;font-weight:bold;">public</span> <span style="color:#000080;font-weight:bold;">actual</span> <span style="color:#000080;font-weight:bold;">final</span> <span style="color:#000080;font-weight:bold;">class</span> <span style="color:#000000;">Foo</span></pre></div><div class='content'><p style='margin-top:0;padding-top:0;'>Doc for expected class Foo</p></div><table class='sections'></table><div class='bottom'><icon src="/org/jetbrains/kotlin/idea/icons/kotlin_file.svg"/> Foo.kt<br/></div>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// PLATFORM: Common
|
||||
// FILE: Foo.kt
|
||||
/**
|
||||
* Doc for expected class Foo
|
||||
*
|
||||
* @param param doc for expected `param` of function `foo`
|
||||
*/
|
||||
expect fun foo(param: String)
|
||||
|
||||
// PLATFORM: Jvm
|
||||
// FILE: Foo.kt
|
||||
// MAIN
|
||||
actual fun foo(pa<caret>ram: String) {}
|
||||
|
||||
//INFO: <div class='definition'><pre><span style="color:#000080;font-weight:bold;">value-parameter</span> <span style="color:#000000;">param</span><span style="">: </span><span style="color:#000000;">String</span></pre></div><div class='content'><p style='margin-top:0;padding-top:0;'>doc for expected <code><span style="">param</span></code> of function <code><span style="">foo</span></code></p></div><table class='sections'></table>
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
package org.jetbrains.kotlin.idea.test
|
||||
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.testFramework.*
|
||||
import org.jetbrains.kotlin.idea.base.test.ModuleStructureSplitter
|
||||
import org.jetbrains.kotlin.idea.framework.KotlinSdkType
|
||||
import org.jetbrains.kotlin.idea.test.util.slashedPath
|
||||
import java.io.File
|
||||
|
||||
abstract class KotlinLightMultiplatformCodeInsightFixtureTestCase : KotlinLightCodeInsightFixtureTestCaseBase() {
|
||||
|
||||
private val exceptions = ArrayList<Throwable>()
|
||||
|
||||
@Deprecated("Migrate to 'testDataDirectory'.", ReplaceWith("testDataDirectory"))
|
||||
final override fun getTestDataPath(): String = testDataDirectory.slashedPath
|
||||
|
||||
open val testDataDirectory: File by lazy {
|
||||
File(TestMetadataUtil.getTestDataPath(javaClass))
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the module structure based on the given file.
|
||||
*
|
||||
* File is expected to have the following structure:
|
||||
* // PLATFORM: (Common|Jvm|Js) [org.jetbrains.kotlin.idea.test.KotlinMultiPlatformProjectDescriptor.PlatformDescriptor]
|
||||
* files
|
||||
*
|
||||
* // FILE: relativePath.kt (relative to the platform's source root)
|
||||
* // MAIN (if this file should be configured in the editor)
|
||||
* file content
|
||||
*
|
||||
* Each file is added to the test project's platform module.
|
||||
*
|
||||
* Returns file which was marked as `MAIN` or null
|
||||
*/
|
||||
fun configureModuleStructure(abstractFilePath: String): VirtualFile? {
|
||||
val map = ModuleStructureSplitter.splitPerModule(File(abstractFilePath))
|
||||
var currentFile: VirtualFile? = null
|
||||
map.forEach { (platform, files) ->
|
||||
val platformDescriptor = when (platform) {
|
||||
"Common" -> KotlinMultiPlatformProjectDescriptor.PlatformDescriptor.COMMON
|
||||
"Jvm" -> KotlinMultiPlatformProjectDescriptor.PlatformDescriptor.JVM
|
||||
"Js" -> KotlinMultiPlatformProjectDescriptor.PlatformDescriptor.JS
|
||||
else -> null
|
||||
}
|
||||
if (platformDescriptor != null) {
|
||||
for (testFile in files) {
|
||||
val virtualFile = VfsTestUtil.createFile(platformDescriptor.sourceRoot()!!, testFile.relativePath, testFile.text)
|
||||
if (testFile.isMain) {
|
||||
currentFile = virtualFile
|
||||
}
|
||||
myFixture.configureFromExistingVirtualFile(virtualFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
return currentFile
|
||||
}
|
||||
|
||||
override fun tearDown() {
|
||||
runAll(
|
||||
{ KotlinMultiPlatformProjectDescriptor.cleanupSourceRoots() },
|
||||
{ KotlinSdkType.removeKotlinSdkInTests() },
|
||||
{ super.tearDown() },
|
||||
)
|
||||
|
||||
if (exceptions.isNotEmpty()) {
|
||||
exceptions.forEach { it.printStackTrace() }
|
||||
throw AssertionError("Exceptions in other threads happened")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getProjectDescriptor(): LightProjectDescriptor = KotlinMultiPlatformProjectDescriptor
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.kotlin.idea.test
|
||||
|
||||
import com.intellij.openapi.application.runWriteAction
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.roots.*
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.ex.temp.TempFileSystem
|
||||
import com.intellij.pom.java.LanguageLevel
|
||||
import com.intellij.testFramework.IdeaTestUtil
|
||||
import com.intellij.testFramework.IndexingTestUtil
|
||||
import org.jetbrains.jps.model.java.JavaSourceRootType
|
||||
import org.jetbrains.kotlin.platform.TargetPlatform
|
||||
import org.jetbrains.kotlin.platform.js.JsPlatforms
|
||||
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
|
||||
|
||||
/**
|
||||
* The project is created with three modules: Common, Jvm -> Common, Js -> Common.
|
||||
*
|
||||
* Currently, no libraries are added.
|
||||
*/
|
||||
object KotlinMultiPlatformProjectDescriptor : KotlinLightProjectDescriptor() {
|
||||
enum class PlatformDescriptor(
|
||||
val moduleName: String,
|
||||
val sourceRootName: String? = null
|
||||
) {
|
||||
COMMON("Common", sourceRootName = "src_common"),
|
||||
JVM("Jvm", sourceRootName = "src_jvm"),
|
||||
JS("Js", sourceRootName = "src_js");
|
||||
|
||||
fun sourceRoot(): VirtualFile? = findRoot(sourceRootName)
|
||||
|
||||
private fun findRoot(rootName: String?): VirtualFile? =
|
||||
if (rootName == null) null
|
||||
else TempFileSystem.getInstance().findFileByPath("/${rootName}")
|
||||
?: throw IllegalStateException("Cannot find temp:///${rootName}")
|
||||
}
|
||||
|
||||
override fun getSdk(): Sdk = IdeaTestUtil.getMockJdk9()
|
||||
|
||||
override fun setUpProject(project: Project, handler: SetupHandler) {
|
||||
super.setUpProject(project, handler)
|
||||
|
||||
runWriteAction {
|
||||
val common = makeModule(project, PlatformDescriptor.COMMON)
|
||||
|
||||
val jvm = makeModule(project, PlatformDescriptor.JVM)
|
||||
ModuleRootModificationUtil.addDependency(jvm, common)
|
||||
|
||||
val js = makeModule(project, PlatformDescriptor.JS)
|
||||
ModuleRootModificationUtil.addDependency(js, common)
|
||||
}
|
||||
IndexingTestUtil.waitUntilIndexesAreReady(project)
|
||||
}
|
||||
|
||||
private fun makeModule(project: Project, descriptor: PlatformDescriptor): Module {
|
||||
val path = "${FileUtil.getTempDirectory()}/${descriptor.moduleName}.iml"
|
||||
val module = createModule(project, path)
|
||||
ModuleRootModificationUtil.updateModel(module) { configureModule(module, it, descriptor) }
|
||||
return module
|
||||
}
|
||||
|
||||
private fun configureModule(module: Module, model: ModifiableRootModel, descriptor: PlatformDescriptor) {
|
||||
model.getModuleExtension(LanguageLevelModuleExtension::class.java).languageLevel = LanguageLevel.HIGHEST
|
||||
model.sdk = sdk
|
||||
if (descriptor.sourceRootName != null) {
|
||||
val sourceRoot = createSourceRoot(module, descriptor.sourceRootName)
|
||||
model.addContentEntry(sourceRoot).addSourceFolder(sourceRoot, JavaSourceRootType.SOURCE)
|
||||
}
|
||||
|
||||
when (descriptor) {
|
||||
PlatformDescriptor.JVM -> {
|
||||
module.createMultiplatformFacetM3(JvmPlatforms.jvm8, false, listOf("Common"), listOf(descriptor.sourceRoot()!!.path))
|
||||
}
|
||||
|
||||
PlatformDescriptor.JS ->
|
||||
module.createMultiplatformFacetM3(JsPlatforms.defaultJsPlatform, false, listOf("Common"), listOf(descriptor.sourceRoot()!!.path))
|
||||
|
||||
PlatformDescriptor.COMMON ->
|
||||
module.createMultiplatformFacetM3(
|
||||
TargetPlatform(
|
||||
setOf(
|
||||
JvmPlatforms.jvm8.single(),
|
||||
JsPlatforms.defaultJsPlatform.single()
|
||||
)
|
||||
), false, emptyList(), listOf(descriptor.sourceRoot()!!.path)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanupSourceRoots() = runWriteAction {
|
||||
PlatformDescriptor.entries.asSequence()
|
||||
.map { it.sourceRoot() }
|
||||
.filterNotNull()
|
||||
.flatMap { it.children.asSequence() }
|
||||
.forEach { it.delete(this) }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -625,7 +625,7 @@ private fun assembleWorkspace(): TWorkspace = workspace {
|
||||
}
|
||||
|
||||
testClass<AbstractQuickDocProviderTest> {
|
||||
model("editor/quickDoc", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$"""))
|
||||
model("editor/quickDoc", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$"""), isRecursive = false)
|
||||
}
|
||||
|
||||
testClass<AbstractSafeDeleteTest> {
|
||||
|
||||
+5
-1
@@ -20,6 +20,7 @@ import org.jetbrains.kotlin.idea.fir.completion.test.handlers.*
|
||||
import org.jetbrains.kotlin.idea.fir.completion.wheigher.AbstractHighLevelWeigherTest
|
||||
import org.jetbrains.kotlin.idea.fir.copyPaste.AbstractFirLiteralKotlinToKotlinCopyPasteTest
|
||||
import org.jetbrains.kotlin.idea.fir.copyPaste.AbstractFirLiteralTextToKotlinCopyPasteTest
|
||||
import org.jetbrains.kotlin.idea.fir.documentation.AbstractFirQuickDocMultiplatformTest
|
||||
import org.jetbrains.kotlin.idea.fir.documentation.AbstractFirQuickDocTest
|
||||
import org.jetbrains.kotlin.idea.fir.externalAnnotations.AbstractK2ExternalAnnotationTest
|
||||
import org.jetbrains.kotlin.idea.fir.findUsages.*
|
||||
@@ -374,7 +375,10 @@ private fun assembleWorkspace(): TWorkspace = workspace {
|
||||
|
||||
testGroup("fir/tests") {
|
||||
testClass<AbstractFirQuickDocTest> {
|
||||
model("../../../idea/tests/testData/editor/quickDoc", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$"""))
|
||||
model("../../../idea/tests/testData/editor/quickDoc", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$"""), isRecursive = false)
|
||||
}
|
||||
testClass<AbstractFirQuickDocMultiplatformTest> {
|
||||
model("../../../idea/tests/testData/editor/quickDoc/multiplatform", pattern = Patterns.forRegex("""^([^_]+)\.(kt|java)$"""))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user