[KAPT] Setup KaptJavaLog writers during initialization.
Migrate KaptJavaLog to not used deprecated constructor in newer JDKs and instead set up the writers during initialization. This enables us to get rid of KaptJavaLog17. Fixes KT-54030
This commit is contained in:
committed by
Alexander Udalov
parent
f4845b8dd9
commit
189be2b117
@@ -329,6 +329,7 @@ tasks.withType<Test> {
|
||||
val jdk10Provider = project.getToolchainLauncherFor(JdkMajorVersion.JDK_10_0).map { it.metadata.installationPath.asFile.absolutePath }
|
||||
val jdk11Provider = project.getToolchainLauncherFor(JdkMajorVersion.JDK_11_0).map { it.metadata.installationPath.asFile.absolutePath }
|
||||
val jdk16Provider = project.getToolchainLauncherFor(JdkMajorVersion.JDK_16_0).map { it.metadata.installationPath.asFile.absolutePath }
|
||||
val jdk17Provider = project.getToolchainLauncherFor(JdkMajorVersion.JDK_17_0).map { it.metadata.installationPath.asFile.absolutePath }
|
||||
val mavenLocalRepo = project.providers.systemProperty("maven.repo.local").forUseAtConfigurationTime().orNull
|
||||
|
||||
// Query required JDKs paths only on execution phase to avoid triggering auto-download on project configuration phase
|
||||
@@ -338,6 +339,7 @@ tasks.withType<Test> {
|
||||
systemProperty("jdk10Home", jdk10Provider.get())
|
||||
systemProperty("jdk11Home", jdk11Provider.get())
|
||||
systemProperty("jdk16Home", jdk16Provider.get())
|
||||
systemProperty("jdk17Home", jdk17Provider.get())
|
||||
if (mavenLocalRepo != null) {
|
||||
systemProperty("maven.repo.local", mavenLocalRepo)
|
||||
}
|
||||
|
||||
+1
-1
@@ -124,7 +124,7 @@ open class Kapt3IT : Kapt3BaseIT() {
|
||||
}
|
||||
|
||||
@DisplayName("Kapt is working with newer JDKs")
|
||||
@JdkVersions(versions = [JavaVersion.VERSION_1_10, JavaVersion.VERSION_11, JavaVersion.VERSION_16])
|
||||
@JdkVersions(versions = [JavaVersion.VERSION_1_10, JavaVersion.VERSION_11, JavaVersion.VERSION_16, JavaVersion.VERSION_17])
|
||||
@GradleWithJdkTest
|
||||
fun doTestSimpleWithCustomJdk(
|
||||
gradleVersion: GradleVersion,
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.jetbrains.kotlin.kapt3.base.incremental.JavaClassCacheManager
|
||||
import org.jetbrains.kotlin.kapt3.base.incremental.SourcesToReprocess
|
||||
import org.jetbrains.kotlin.kapt3.base.javac.*
|
||||
import org.jetbrains.kotlin.kapt3.base.util.KaptLogger
|
||||
import org.jetbrains.kotlin.kapt3.base.util.isJava17OrLater
|
||||
import org.jetbrains.kotlin.kapt3.base.util.isJava9OrLater
|
||||
import org.jetbrains.kotlin.kapt3.base.util.putJavacOption
|
||||
import java.io.Closeable
|
||||
@@ -39,22 +38,10 @@ open class KaptContext(val options: KaptOptions, val withJdk: Boolean, val logge
|
||||
private fun preregisterLog(context: Context) {
|
||||
val interceptorData = KaptJavaLogBase.DiagnosticInterceptorData()
|
||||
context.put(Log.logKey, Context.Factory<Log> { newContext ->
|
||||
if (isJava17OrLater()) {
|
||||
newContext.put(Log.outKey, logger.infoWriter)
|
||||
val errKey = (Log::class.java.fields.firstOrNull() { it.name == "errKey" }
|
||||
?: error("Can't find errKey field in Log.class")).get(null)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
newContext.put(errKey as Context.Key<java.io.PrintWriter>, logger.errorWriter)
|
||||
KaptJavaLog17(
|
||||
options.projectBaseDir, newContext,
|
||||
interceptorData, options[KaptFlag.MAP_DIAGNOSTIC_LOCATIONS]
|
||||
)
|
||||
} else {
|
||||
KaptJavaLog(
|
||||
options.projectBaseDir, newContext, logger.errorWriter, logger.warnWriter, logger.infoWriter,
|
||||
interceptorData, options[KaptFlag.MAP_DIAGNOSTIC_LOCATIONS]
|
||||
)
|
||||
}
|
||||
KaptJavaLog(
|
||||
options.projectBaseDir, newContext, logger.errorWriter, logger.warnWriter, logger.infoWriter,
|
||||
interceptorData, options[KaptFlag.MAP_DIAGNOSTIC_LOCATIONS]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -40,12 +40,14 @@ class KaptJavaLog(
|
||||
noticeWriter: PrintWriter,
|
||||
override val interceptorData: KaptJavaLogBase.DiagnosticInterceptorData,
|
||||
private val mapDiagnosticLocations: Boolean
|
||||
) : Log(context, errWriter, warnWriter, noticeWriter), KaptJavaLogBase {
|
||||
) : Log(context), KaptJavaLogBase {
|
||||
private val stubLineInfo = KaptStubLineInformation()
|
||||
private val javacMessages = JavacMessages.instance(context)
|
||||
|
||||
init {
|
||||
context.put(Log.outKey, noticeWriter)
|
||||
setWriter(WriterKind.ERROR, errWriter)
|
||||
setWriter(WriterKind.WARNING, warnWriter)
|
||||
setWriter(WriterKind.NOTICE, noticeWriter)
|
||||
}
|
||||
|
||||
override val reportedDiagnostics: List<JCDiagnostic>
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
/*
|
||||
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package org.jetbrains.kotlin.kapt3.base.javac
|
||||
|
||||
import com.sun.tools.javac.tree.JCTree
|
||||
import com.sun.tools.javac.util.*
|
||||
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticType
|
||||
import org.jetbrains.kotlin.kapt3.base.stubs.KaptStubLineInformation
|
||||
import org.jetbrains.kotlin.kapt3.base.stubs.KotlinPosition
|
||||
import java.io.*
|
||||
import javax.tools.Diagnostic
|
||||
|
||||
// It's copy/paste as is of KaptJavaLog
|
||||
// TODO: most likely KaptJavaLog could be substituted with this version.
|
||||
// The only difference that is writers are passed through context
|
||||
class KaptJavaLog17(
|
||||
private val projectBaseDir: File?,
|
||||
context: Context,
|
||||
override val interceptorData: KaptJavaLogBase.DiagnosticInterceptorData,
|
||||
private val mapDiagnosticLocations: Boolean
|
||||
) : Log(context), KaptJavaLogBase {
|
||||
private val stubLineInfo = KaptStubLineInformation()
|
||||
private val javacMessages = JavacMessages.instance(context)
|
||||
|
||||
override val reportedDiagnostics: List<JCDiagnostic>
|
||||
get() = _reportedDiagnostics
|
||||
|
||||
private val _reportedDiagnostics = mutableListOf<JCDiagnostic>()
|
||||
|
||||
override fun flush(kind: WriterKind?) {
|
||||
super.flush(kind)
|
||||
|
||||
val diagnosticKind = when (kind) {
|
||||
WriterKind.ERROR -> JCDiagnostic.DiagnosticType.ERROR
|
||||
WriterKind.WARNING -> JCDiagnostic.DiagnosticType.WARNING
|
||||
WriterKind.NOTICE -> JCDiagnostic.DiagnosticType.NOTE
|
||||
else -> return
|
||||
}
|
||||
|
||||
_reportedDiagnostics.removeAll { it.type == diagnosticKind }
|
||||
}
|
||||
|
||||
override fun flush() {
|
||||
super.flush()
|
||||
_reportedDiagnostics.clear()
|
||||
}
|
||||
|
||||
override fun report(diagnostic: JCDiagnostic) {
|
||||
if (diagnostic.type == JCDiagnostic.DiagnosticType.ERROR && diagnostic.code in IGNORED_DIAGNOSTICS) {
|
||||
return
|
||||
}
|
||||
|
||||
if (diagnostic.type == JCDiagnostic.DiagnosticType.WARNING
|
||||
&& diagnostic.code == "compiler.warn.proc.unmatched.processor.options"
|
||||
&& diagnostic.args.singleOrNull() == "[kapt.kotlin.generated]"
|
||||
) {
|
||||
// Do not report the warning about "kapt.kotlin.generated" option being ignored if it's the only ignored option
|
||||
return
|
||||
}
|
||||
|
||||
val targetElement = diagnostic.diagnosticPosition
|
||||
val sourceFile = interceptorData.files[diagnostic.source]
|
||||
|
||||
if (diagnostic.code.contains("err.cant.resolve") && targetElement != null) {
|
||||
if (sourceFile != null) {
|
||||
val insideImports = targetElement.tree in sourceFile.imports
|
||||
// Ignore resolve errors in import statements
|
||||
if (insideImports) return
|
||||
}
|
||||
}
|
||||
|
||||
if (mapDiagnosticLocations && sourceFile != null && targetElement?.tree != null) {
|
||||
val kotlinPosition = stubLineInfo.getPositionInKotlinFile(sourceFile, targetElement.tree)
|
||||
val kotlinFile = kotlinPosition?.let { getKotlinSourceFile(it) }
|
||||
if (kotlinPosition != null && kotlinFile != null) {
|
||||
val flags = JCDiagnostic.DiagnosticFlag.values().filterTo(mutableSetOf(), diagnostic::isFlagSet)
|
||||
|
||||
val kotlinDiagnostic = diags.create(
|
||||
diagnostic.type,
|
||||
diagnostic.lintCategory,
|
||||
flags,
|
||||
DiagnosticSource(KotlinFileObject(kotlinFile), this),
|
||||
JCDiagnostic.SimpleDiagnosticPosition(kotlinPosition.pos),
|
||||
diagnostic.code.stripCompilerKeyPrefix(),
|
||||
*diagnostic.args
|
||||
)
|
||||
|
||||
reportDiagnostic(kotlinDiagnostic)
|
||||
|
||||
// Avoid reporting the diagnostic twice
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
reportDiagnostic(diagnostic)
|
||||
}
|
||||
|
||||
private fun String.stripCompilerKeyPrefix(): String {
|
||||
for (kind in listOf("err", "warn", "misc", "note")) {
|
||||
val prefix = "compiler.$kind."
|
||||
if (startsWith(prefix)) {
|
||||
return drop(prefix.length)
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun reportDiagnostic(diagnostic: JCDiagnostic) {
|
||||
if (diagnostic.kind == Diagnostic.Kind.ERROR) {
|
||||
val oldErrors = nerrors
|
||||
super.report(diagnostic)
|
||||
if (nerrors > oldErrors) {
|
||||
_reportedDiagnostics += diagnostic
|
||||
}
|
||||
} else if (diagnostic.kind == Diagnostic.Kind.WARNING) {
|
||||
val oldWarnings = nwarnings
|
||||
super.report(diagnostic)
|
||||
if (nwarnings > oldWarnings) {
|
||||
_reportedDiagnostics += diagnostic
|
||||
}
|
||||
} else {
|
||||
super.report(diagnostic)
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeDiagnostic(diagnostic: JCDiagnostic) {
|
||||
if (hasDiagnosticListener()) {
|
||||
diagListener.report(diagnostic)
|
||||
return
|
||||
}
|
||||
|
||||
val writer = when (diagnostic.type) {
|
||||
DiagnosticType.FRAGMENT, null -> kotlin.error("Invalid root diagnostic type: ${diagnostic.type}")
|
||||
DiagnosticType.NOTE -> super.getWriter(WriterKind.NOTICE)
|
||||
DiagnosticType.WARNING -> super.getWriter(WriterKind.WARNING)
|
||||
DiagnosticType.ERROR -> super.getWriter(WriterKind.ERROR)
|
||||
}
|
||||
|
||||
val formattedMessage = diagnosticFormatter.format(diagnostic, javacMessages.currentLocale)
|
||||
.lines()
|
||||
.joinToString(LINE_SEPARATOR, postfix = LINE_SEPARATOR) { original ->
|
||||
// Kotlin location is put as a sub-diagnostic, so the formatter indents it with four additional spaces (6 in total).
|
||||
// It looks weird, especially in the build log inside IntelliJ, so let's make things a bit better.
|
||||
val trimmed = original.trimStart()
|
||||
// Typically, javac places additional details about the diagnostics indented by two spaces
|
||||
if (trimmed.startsWith(KOTLIN_LOCATION_PREFIX)) " " + trimmed else original
|
||||
}
|
||||
|
||||
writer.print(formattedMessage)
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
private fun getKotlinSourceFile(pos: KotlinPosition): File? {
|
||||
return if (pos.isRelativePath) {
|
||||
val basePath = this.projectBaseDir
|
||||
if (basePath != null) File(basePath, pos.path) else null
|
||||
} else {
|
||||
File(pos.path)
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun <T : JCTree> Iterable<T>.contains(element: JCTree?): Boolean {
|
||||
if (element == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
var found = false
|
||||
val visitor = object : JCTree.Visitor() {
|
||||
override fun visitImport(that: JCTree.JCImport) {
|
||||
super.visitImport(that)
|
||||
if (!found) that.qualid.accept(this)
|
||||
}
|
||||
|
||||
override fun visitSelect(that: JCTree.JCFieldAccess) {
|
||||
super.visitSelect(that)
|
||||
if (!found) that.selected.accept(this)
|
||||
}
|
||||
|
||||
override fun visitTree(that: JCTree) {
|
||||
if (!found && element == that) found = true
|
||||
}
|
||||
}
|
||||
this.forEach { if (!found) it.accept(visitor) }
|
||||
return found
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LINE_SEPARATOR: String = System.getProperty("line.separator")
|
||||
private val KOTLIN_LOCATION_PREFIX = "Kotlin location: "
|
||||
|
||||
private val IGNORED_DIAGNOSTICS = setOf(
|
||||
"compiler.err.name.clash.same.erasure",
|
||||
"compiler.err.name.clash.same.erasure.no.override",
|
||||
"compiler.err.name.clash.same.erasure.no.override.1",
|
||||
"compiler.err.name.clash.same.erasure.no.hide",
|
||||
"compiler.err.already.defined",
|
||||
"compiler.err.annotation.type.not.applicable",
|
||||
"compiler.err.doesnt.exist",
|
||||
"compiler.err.cant.resolve.location",
|
||||
"compiler.err.duplicate.annotation.missing.container",
|
||||
"compiler.err.not.def.access.package.cant.access",
|
||||
"compiler.err.package.not.visible",
|
||||
"compiler.err.not.def.public.cant.access"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ testsJar {}
|
||||
|
||||
kaptTestTask("test", JavaLanguageVersion.of(8))
|
||||
kaptTestTask("testJdk11", JavaLanguageVersion.of(11))
|
||||
kaptTestTask("testJdk17", JavaLanguageVersion.of(17))
|
||||
|
||||
fun Project.kaptTestTask(name: String, javaLanguageVersion: JavaLanguageVersion) {
|
||||
val service = extensions.getByType<JavaToolchainService>()
|
||||
|
||||
+39
-11
@@ -5,6 +5,8 @@
|
||||
|
||||
package org.jetbrains.kotlin.kapt3.test.integration
|
||||
|
||||
import org.jetbrains.kotlin.analyzer.CompilationErrorException
|
||||
import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
|
||||
import org.jetbrains.kotlin.kapt3.javac.KaptJavaFileObject
|
||||
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertEquals
|
||||
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
|
||||
@@ -33,6 +35,7 @@ abstract class AbstractKotlinKapt3IntegrationTestBase(private val testInfo: Test
|
||||
name: String,
|
||||
vararg supportedAnnotations: String,
|
||||
options: Map<String, String> = emptyMap(),
|
||||
expectFailure: Boolean = false,
|
||||
process: (Set<TypeElement>, RoundEnvironment, ProcessingEnvironment, Kapt3ExtensionForTests) -> Unit
|
||||
) {
|
||||
val file = File(TEST_DATA_DIR, "$name.kt")
|
||||
@@ -43,7 +46,12 @@ abstract class AbstractKotlinKapt3IntegrationTestBase(private val testInfo: Test
|
||||
process
|
||||
).apply {
|
||||
initTestInfo(testInfo)
|
||||
runTest(file.absolutePath)
|
||||
try {
|
||||
runTest(file.absolutePath)
|
||||
if (expectFailure) throw AssertionError("Expected compilation to fail, but it didn't.")
|
||||
} catch (ex: CompilationErrorException) {
|
||||
if (!expectFailure) throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,19 +139,21 @@ abstract class AbstractKotlinKapt3IntegrationTestBase(private val testInfo: Test
|
||||
}
|
||||
|
||||
private fun List<LoggingMessageCollector.Message>.assertContainsDiagnostic(
|
||||
message: String
|
||||
message: String,
|
||||
severity: CompilerMessageSeverity? = null
|
||||
) {
|
||||
assertTrue(
|
||||
any {
|
||||
it.message.contains(message)
|
||||
any { msg ->
|
||||
(severity?.let { it == msg.severity } ?: true) && msg.message.contains(message)
|
||||
}
|
||||
) {
|
||||
"""
|
||||
Didn't find expected diagnostic message.
|
||||
Expected: $message
|
||||
Diagnostics:
|
||||
${this.joinToString("\n") { "${it.severity}: ${it.message}" }}
|
||||
""".trimIndent()
|
||||
|Didn't find expected diagnostic message.
|
||||
|Expected: $message
|
||||
|Severity: ${severity ?: "ANY"}
|
||||
|Diagnostics:
|
||||
|${this.joinToString("\n") { "${it.severity}: ${it.message}" }}
|
||||
""".trimMargin()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,12 +161,14 @@ abstract class AbstractKotlinKapt3IntegrationTestBase(private val testInfo: Test
|
||||
private fun diagnosticsTest(
|
||||
name: String,
|
||||
vararg supportedAnnotations: String,
|
||||
expectFailure: Boolean = false,
|
||||
process: (Set<TypeElement>, RoundEnvironment, ProcessingEnvironment) -> Unit
|
||||
): List<LoggingMessageCollector.Message> {
|
||||
lateinit var messageCollector: LoggingMessageCollector
|
||||
test(
|
||||
name = name,
|
||||
supportedAnnotations = supportedAnnotations
|
||||
supportedAnnotations = supportedAnnotations,
|
||||
expectFailure = expectFailure,
|
||||
) { typeElements, roundEnv, processingEnv, kaptExtension ->
|
||||
messageCollector = kaptExtension.messageCollector
|
||||
process(typeElements, roundEnv, processingEnv)
|
||||
@@ -211,4 +223,20 @@ abstract class AbstractKotlinKapt3IntegrationTestBase(private val testInfo: Test
|
||||
assertEquals("someLong", constructors[1].parameters[1].simpleName.toString())
|
||||
assertEquals("someString", constructors[1].parameters[2].simpleName.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLog() {
|
||||
val diagnostics = diagnosticsTest(
|
||||
name = "Log",
|
||||
supportedAnnotations = arrayOf("*"),
|
||||
expectFailure = true
|
||||
) { _, _, env ->
|
||||
env.messager.printMessage(Diagnostic.Kind.ERROR, "a error from processor")
|
||||
env.messager.printMessage(Diagnostic.Kind.WARNING, "a warning from processor")
|
||||
env.messager.printMessage(Diagnostic.Kind.NOTE, "a note from processor")
|
||||
}
|
||||
diagnostics.assertContainsDiagnostic("error: a error from processor", CompilerMessageSeverity.ERROR)
|
||||
diagnostics.assertContainsDiagnostic("warning: a warning from processor", CompilerMessageSeverity.STRONG_WARNING)
|
||||
diagnostics.assertContainsDiagnostic("Note: a note from processor", CompilerMessageSeverity.INFO)
|
||||
}
|
||||
}
|
||||
+6
@@ -9,6 +9,7 @@ import org.jetbrains.kotlin.kapt3.test.JvmCompilerWithKaptFacade
|
||||
import org.jetbrains.kotlin.kapt3.test.KaptContextBinaryArtifact
|
||||
import org.jetbrains.kotlin.kapt3.test.KaptEnvironmentConfigurator
|
||||
import org.jetbrains.kotlin.kapt3.test.KaptTestDirectives
|
||||
import org.jetbrains.kotlin.kapt3.util.doOpenInternalPackagesIfRequired
|
||||
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
|
||||
import org.jetbrains.kotlin.test.TargetBackend
|
||||
import org.jetbrains.kotlin.test.bind
|
||||
@@ -28,6 +29,11 @@ class AbstractKotlinKapt3IntegrationTestRunner(
|
||||
private val supportedAnnotations: List<String>,
|
||||
private val process: (Set<TypeElement>, RoundEnvironment, ProcessingEnvironment, Kapt3ExtensionForTests) -> Unit
|
||||
) : AbstractKotlinCompilerWithTargetBackendTest(targetBackend) {
|
||||
|
||||
init {
|
||||
doOpenInternalPackagesIfRequired()
|
||||
}
|
||||
|
||||
override fun TestConfigurationBuilder.configuration() {
|
||||
globalDefaults {
|
||||
frontend = FrontendKinds.ClassicFrontend
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import java.lang.System;
|
||||
|
||||
@kotlin.Metadata()
|
||||
public final class Dummy {
|
||||
|
||||
public Dummy() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////
|
||||
|
||||
package error;
|
||||
|
||||
public final class NonExistentClass {
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
class Dummy
|
||||
+6
@@ -39,6 +39,12 @@ public class IrKotlinKaptContextTestGenerated extends AbstractIrKotlinKaptContex
|
||||
runTest("plugins/kapt3/kapt3-compiler/testData/kotlinRunner/ErrorLocationMapping.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("Log.kt")
|
||||
public void testLog() throws Exception {
|
||||
runTest("plugins/kapt3/kapt3-compiler/testData/kotlinRunner/Log.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("NestedClasses.kt")
|
||||
public void testNestedClasses() throws Exception {
|
||||
|
||||
+6
@@ -39,6 +39,12 @@ public class KotlinKaptContextTestGenerated extends AbstractKotlinKaptContextTes
|
||||
runTest("plugins/kapt3/kapt3-compiler/testData/kotlinRunner/ErrorLocationMapping.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("Log.kt")
|
||||
public void testLog() throws Exception {
|
||||
runTest("plugins/kapt3/kapt3-compiler/testData/kotlinRunner/Log.kt");
|
||||
}
|
||||
|
||||
@Test
|
||||
@TestMetadata("NestedClasses.kt")
|
||||
public void testNestedClasses() throws Exception {
|
||||
|
||||
Reference in New Issue
Block a user