Augment. Report augmentationReturnTypeMismatch.
Add a compile-time diagnostic for augmentations that declare an explicit return type different from the introductory declaration. Check top-level functions, methods, and getters by comparing the augmentation annotation against the introductory element return type using normal type equality. Keep executable element return types initialized from the first fragment only, so later augmentation fragments cannot overwrite the introductory signature before validation. This also keeps synthetic getter/setter variables based on the introductory declaration. Change-Id: I08d55497e235ed2619a6915e0060bd5d8c8a45b6 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/502200 Reviewed-by: Samuel Rawlins <srawlins@google.com> Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
committed by
dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent
8028bd03fb
commit
6d6b2ae57f
@@ -243,6 +243,8 @@ augmentation_of_different_declaration_kind:
|
||||
status: noFix
|
||||
augmentation_of_mixin_application_class:
|
||||
status: needsEvaluation
|
||||
augmentation_return_type_mismatch:
|
||||
status: needsEvaluation
|
||||
augmentation_type_parameter_bound:
|
||||
status: noFix
|
||||
augmentation_type_parameter_count:
|
||||
|
||||
@@ -956,6 +956,28 @@ const DiagnosticWithoutArguments augmentationOfMixinApplicationClass =
|
||||
expectedTypes: [],
|
||||
);
|
||||
|
||||
/// Parameters:
|
||||
/// Type expectedType: the return type of the declaration
|
||||
/// Type actualType: the return type of the augmentation
|
||||
const DiagnosticWithArguments<
|
||||
LocatableDiagnostic Function({
|
||||
required DartType expectedType,
|
||||
required DartType actualType,
|
||||
})
|
||||
>
|
||||
augmentationReturnTypeMismatch = DiagnosticWithArguments(
|
||||
name: 'augmentation_return_type_mismatch',
|
||||
problemMessage:
|
||||
"The augmentation's return type '{1}' must be the same as the introductory "
|
||||
"declaration's return type '{0}'.",
|
||||
correctionMessage:
|
||||
"Try changing the augmentation's return type to match the declaration.",
|
||||
type: DiagnosticType.COMPILE_TIME_ERROR,
|
||||
uniqueName: 'augmentation_return_type_mismatch',
|
||||
withArguments: _withArgumentsAugmentationReturnTypeMismatch,
|
||||
expectedTypes: [ExpectedType.type, ExpectedType.type],
|
||||
);
|
||||
|
||||
/// No parameters.
|
||||
const DiagnosticWithoutArguments augmentationTypeParameterBound =
|
||||
DiagnosticWithoutArgumentsImpl(
|
||||
@@ -18279,6 +18301,16 @@ LocatableDiagnostic _withArgumentsAugmentationOfDifferentDeclarationKind({
|
||||
]);
|
||||
}
|
||||
|
||||
LocatableDiagnostic _withArgumentsAugmentationReturnTypeMismatch({
|
||||
required DartType expectedType,
|
||||
required DartType actualType,
|
||||
}) {
|
||||
return LocatableDiagnosticImpl(diag.augmentationReturnTypeMismatch, [
|
||||
expectedType,
|
||||
actualType,
|
||||
]);
|
||||
}
|
||||
|
||||
LocatableDiagnostic _withArgumentsAugmentedExpressionNotOperator({
|
||||
required String operator,
|
||||
}) {
|
||||
|
||||
@@ -74,6 +74,7 @@ const List<DiagnosticCode> diagnosticCodeValues = [
|
||||
diag.augmentationModifierMissing,
|
||||
diag.augmentationOfDifferentDeclarationKind,
|
||||
diag.augmentationOfMixinApplicationClass,
|
||||
diag.augmentationReturnTypeMismatch,
|
||||
diag.augmentationTypeParameterBound,
|
||||
diag.augmentationTypeParameterCount,
|
||||
diag.augmentationTypeParameterName,
|
||||
|
||||
@@ -1136,6 +1136,10 @@ class ErrorVerifier extends RecursiveAstVisitor<void>
|
||||
nameOrKeywordToken: node.name,
|
||||
typeParameterList: node.functionExpression.typeParameters,
|
||||
);
|
||||
_checkForAugmentationReturnTypeMismatch(
|
||||
fragment: fragment,
|
||||
returnTypeNode: node.returnType,
|
||||
);
|
||||
|
||||
if (element.enclosingElement is! LibraryElement) {
|
||||
_hiddenElements!.declare(element);
|
||||
@@ -1382,6 +1386,10 @@ class ErrorVerifier extends RecursiveAstVisitor<void>
|
||||
nameOrKeywordToken: node.name,
|
||||
typeParameterList: node.typeParameters,
|
||||
);
|
||||
_checkForAugmentationReturnTypeMismatch(
|
||||
fragment: fragment,
|
||||
returnTypeNode: node.returnType,
|
||||
);
|
||||
|
||||
_withEnclosingExecutable(
|
||||
element,
|
||||
@@ -2555,6 +2563,29 @@ class ErrorVerifier extends RecursiveAstVisitor<void>
|
||||
}
|
||||
}
|
||||
|
||||
void _checkForAugmentationReturnTypeMismatch({
|
||||
required ExecutableFragmentImpl fragment,
|
||||
required TypeAnnotation? returnTypeNode,
|
||||
}) {
|
||||
if (!fragment.isAugmentation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (returnTypeNode == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var expectedType = fragment.element.returnType;
|
||||
var actualType = returnTypeNode.typeOrThrow;
|
||||
if (!typeSystem.isEqualTo(actualType, expectedType)) {
|
||||
diagnosticReporter.report(
|
||||
diag.augmentationReturnTypeMismatch
|
||||
.withArguments(expectedType: expectedType, actualType: actualType)
|
||||
.at(returnTypeNode),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _checkForAugmentationTypeParameters({
|
||||
required FragmentImpl fragment,
|
||||
required List<TypeParameterFragmentImpl> firstTypeParameters,
|
||||
|
||||
@@ -281,21 +281,22 @@ class TypesBuilder {
|
||||
}
|
||||
|
||||
void _functionDeclaration(FunctionDeclarationImpl node) {
|
||||
var returnType = node.returnType?.type;
|
||||
if (returnType == null) {
|
||||
if (node.isSetter) {
|
||||
returnType = _voidType;
|
||||
} else {
|
||||
returnType = _dynamicType;
|
||||
}
|
||||
}
|
||||
|
||||
var fragment = node.declaredFragment!;
|
||||
var element = fragment.element;
|
||||
|
||||
if (fragment.previousFragment == null) {
|
||||
var returnType = node.returnType?.type;
|
||||
if (returnType == null) {
|
||||
if (node.isSetter) {
|
||||
returnType = _voidType;
|
||||
} else {
|
||||
returnType = _dynamicType;
|
||||
}
|
||||
}
|
||||
|
||||
element.returnType = returnType;
|
||||
_setSyntheticVariableType(element);
|
||||
}
|
||||
_setSyntheticVariableType(element);
|
||||
}
|
||||
|
||||
void _functionTypeAlias(FunctionTypeAliasImpl node) {
|
||||
@@ -346,21 +347,24 @@ class TypesBuilder {
|
||||
}
|
||||
|
||||
void _methodDeclaration(MethodDeclarationImpl node) {
|
||||
var returnType = node.returnType?.type;
|
||||
if (returnType == null) {
|
||||
if (node.isSetter) {
|
||||
returnType = _voidType;
|
||||
} else if (node.isOperator && node.name.lexeme == '[]=') {
|
||||
returnType = _voidType;
|
||||
} else {
|
||||
returnType = _dynamicType;
|
||||
}
|
||||
}
|
||||
|
||||
var fragment = node.declaredFragment!;
|
||||
var element = fragment.element;
|
||||
element.returnType = returnType;
|
||||
_setSyntheticVariableType(element);
|
||||
|
||||
if (fragment.previousFragment == null) {
|
||||
var returnType = node.returnType?.type;
|
||||
if (returnType == null) {
|
||||
if (node.isSetter) {
|
||||
returnType = _voidType;
|
||||
} else if (node.isOperator && node.name.lexeme == '[]=') {
|
||||
returnType = _voidType;
|
||||
} else {
|
||||
returnType = _dynamicType;
|
||||
}
|
||||
}
|
||||
|
||||
element.returnType = returnType;
|
||||
_setSyntheticVariableType(element);
|
||||
}
|
||||
}
|
||||
|
||||
void _mixinDeclaration(MixinDeclarationImpl node) {
|
||||
|
||||
@@ -1876,6 +1876,15 @@ CompileTimeErrorCode:
|
||||
problemMessage: "Mixin application classes can't be augmented."
|
||||
correctionMessage: Try removing the 'augment' keyword, or making the target a normal class.
|
||||
hasPublishedDocs: false
|
||||
augmentationReturnTypeMismatch:
|
||||
type: compileTimeError
|
||||
parameters:
|
||||
Type expectedType: the return type of the declaration
|
||||
Type actualType: the return type of the augmentation
|
||||
experiment: augmentations
|
||||
problemMessage: "The augmentation's return type '#actualType' must be the same as the introductory declaration's return type '#expectedType'."
|
||||
correctionMessage: Try changing the augmentation's return type to match the declaration.
|
||||
hasPublishedDocs: false
|
||||
augmentedExpressionIsNotSetter:
|
||||
type: compileTimeError
|
||||
parameters: none
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
|
||||
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||
|
||||
import '../dart/resolution/context_collection_resolution.dart';
|
||||
|
||||
main() {
|
||||
defineReflectiveSuite(() {
|
||||
defineReflectiveTests(AugmentationReturnTypeMismatchTest);
|
||||
});
|
||||
}
|
||||
|
||||
@reflectiveTest
|
||||
class AugmentationReturnTypeMismatchTest extends PubPackageResolutionTest {
|
||||
test_class_getter_int_String() async {
|
||||
await assertErrorsInCode(
|
||||
r'''
|
||||
class A {
|
||||
int get foo => 0;
|
||||
}
|
||||
|
||||
augment class A {
|
||||
augment String get foo => '';
|
||||
}
|
||||
''',
|
||||
[
|
||||
error(diag.augmentationReturnTypeMismatch, 61, 6),
|
||||
error(diag.returnOfInvalidTypeFromFunction, 79, 2),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test_class_method_void_int() async {
|
||||
await assertErrorsInCode(
|
||||
r'''
|
||||
class A {
|
||||
void foo() {}
|
||||
}
|
||||
|
||||
augment class A {
|
||||
augment int foo() => 0;
|
||||
}
|
||||
''',
|
||||
[error(diag.augmentationReturnTypeMismatch, 57, 3)],
|
||||
);
|
||||
}
|
||||
|
||||
test_class_method_void_void() async {
|
||||
await assertNoErrorsInCode(r'''
|
||||
class A {
|
||||
void foo() {}
|
||||
}
|
||||
|
||||
augment class A {
|
||||
augment void foo() {}
|
||||
}
|
||||
''');
|
||||
}
|
||||
|
||||
test_extension_getter_int_String() async {
|
||||
await assertErrorsInCode(
|
||||
r'''
|
||||
extension E on int {
|
||||
int get foo => 0;
|
||||
}
|
||||
|
||||
augment extension E {
|
||||
augment String get foo => '';
|
||||
}
|
||||
''',
|
||||
[
|
||||
error(diag.augmentationReturnTypeMismatch, 76, 6),
|
||||
error(diag.returnOfInvalidTypeFromFunction, 94, 2),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test_extension_method_void_int() async {
|
||||
await assertErrorsInCode(
|
||||
r'''
|
||||
extension E on int {
|
||||
void foo() {}
|
||||
}
|
||||
|
||||
augment extension E {
|
||||
augment int foo() => 0;
|
||||
}
|
||||
''',
|
||||
[error(diag.augmentationReturnTypeMismatch, 72, 3)],
|
||||
);
|
||||
}
|
||||
|
||||
test_extensionType_getter_int_String() async {
|
||||
await assertErrorsInCode(
|
||||
r'''
|
||||
extension type A(int it) {
|
||||
int get foo => 0;
|
||||
}
|
||||
|
||||
augment extension type A(int it) {
|
||||
augment String get foo => '';
|
||||
}
|
||||
''',
|
||||
[
|
||||
error(diag.augmentationReturnTypeMismatch, 95, 6),
|
||||
error(diag.returnOfInvalidTypeFromFunction, 113, 2),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test_extensionType_method_void_int() async {
|
||||
await assertErrorsInCode(
|
||||
r'''
|
||||
extension type A(int it) {
|
||||
void foo() {}
|
||||
}
|
||||
|
||||
augment extension type A(int it) {
|
||||
augment int foo() => 0;
|
||||
}
|
||||
''',
|
||||
[error(diag.augmentationReturnTypeMismatch, 91, 3)],
|
||||
);
|
||||
}
|
||||
|
||||
test_mixin_getter_int_String() async {
|
||||
await assertErrorsInCode(
|
||||
r'''
|
||||
mixin M {
|
||||
int get foo => 0;
|
||||
}
|
||||
|
||||
augment mixin M {
|
||||
augment String get foo => '';
|
||||
}
|
||||
''',
|
||||
[
|
||||
error(diag.augmentationReturnTypeMismatch, 61, 6),
|
||||
error(diag.returnOfInvalidTypeFromFunction, 79, 2),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test_mixin_method_void_int() async {
|
||||
await assertErrorsInCode(
|
||||
r'''
|
||||
mixin M {
|
||||
void foo() {}
|
||||
}
|
||||
|
||||
augment mixin M {
|
||||
augment int foo() => 0;
|
||||
}
|
||||
''',
|
||||
[error(diag.augmentationReturnTypeMismatch, 57, 3)],
|
||||
);
|
||||
}
|
||||
|
||||
test_topLevelFunction_int_int_withImportPrefix() async {
|
||||
await assertNoErrorsInCode(r'''
|
||||
import 'dart:core';
|
||||
import 'dart:core' as core;
|
||||
|
||||
int foo() => 0;
|
||||
|
||||
augment core.int foo() => 0;
|
||||
''');
|
||||
}
|
||||
|
||||
test_topLevelFunction_void_int() async {
|
||||
await assertErrorsInCode(
|
||||
r'''
|
||||
void foo() {}
|
||||
|
||||
augment int foo() => 0;
|
||||
''',
|
||||
[error(diag.augmentationReturnTypeMismatch, 23, 3)],
|
||||
);
|
||||
}
|
||||
|
||||
test_topLevelFunction_void_int_viaTypeAlias() async {
|
||||
await assertErrorsInCode(
|
||||
r'''
|
||||
typedef IntAlias = int;
|
||||
|
||||
void foo() {}
|
||||
|
||||
augment IntAlias foo() => 0;
|
||||
''',
|
||||
[error(diag.augmentationReturnTypeMismatch, 48, 8)],
|
||||
);
|
||||
}
|
||||
|
||||
test_topLevelFunction_void_int_withImportPrefix() async {
|
||||
await assertErrorsInCode(
|
||||
r'''
|
||||
import 'dart:core' as core;
|
||||
void foo() {}
|
||||
|
||||
augment core.int foo() => 0;
|
||||
''',
|
||||
[error(diag.augmentationReturnTypeMismatch, 51, 8)],
|
||||
);
|
||||
}
|
||||
|
||||
test_topLevelFunction_void_nothing() async {
|
||||
await assertNoErrorsInCode(r'''
|
||||
void foo() {}
|
||||
|
||||
augment foo() {}
|
||||
''');
|
||||
}
|
||||
|
||||
test_topLevelFunction_void_void() async {
|
||||
await assertNoErrorsInCode(r'''
|
||||
void foo() {}
|
||||
|
||||
augment void foo() {}
|
||||
''');
|
||||
}
|
||||
|
||||
test_topLevelFunction_void_void_viaTypeAlias() async {
|
||||
await assertNoErrorsInCode(r'''
|
||||
typedef VoidAlias = void;
|
||||
|
||||
void foo() {}
|
||||
|
||||
augment VoidAlias foo() {}
|
||||
''');
|
||||
}
|
||||
|
||||
test_topLevelGetter_int_String() async {
|
||||
await assertErrorsInCode(
|
||||
r'''
|
||||
int get foo => 0;
|
||||
|
||||
augment String get foo => '';
|
||||
''',
|
||||
[
|
||||
error(diag.augmentationReturnTypeMismatch, 27, 6),
|
||||
error(diag.returnOfInvalidTypeFromFunction, 45, 2),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,8 @@ import 'augmentation_of_different_declaration_kind_test.dart'
|
||||
as augmentation_of_different_declaration_kind;
|
||||
import 'augmentation_of_mixin_application_class_test.dart'
|
||||
as augmentation_of_mixin_application_class;
|
||||
import 'augmentation_return_type_mismatch_test.dart'
|
||||
as augmentation_return_type_mismatch;
|
||||
import 'augmentation_type_parameter_bound_test.dart'
|
||||
as augmentation_type_parameter_bound;
|
||||
import 'augmentation_type_parameter_count_test.dart'
|
||||
@@ -990,6 +992,7 @@ main() {
|
||||
augmentation_modifier_missing.main();
|
||||
augmentation_of_different_declaration_kind.main();
|
||||
augmentation_of_mixin_application_class.main();
|
||||
augmentation_return_type_mismatch.main();
|
||||
augmentation_type_parameter_bound.main();
|
||||
augmentation_type_parameter_count.main();
|
||||
augmentation_type_parameter_name.main();
|
||||
|
||||
@@ -33153,6 +33153,61 @@ library
|
||||
''');
|
||||
}
|
||||
|
||||
test_getter_augmentation_chain_returnType_int_String() async {
|
||||
var library = await buildLibrary(r'''
|
||||
class A {
|
||||
int get foo => 0;
|
||||
}
|
||||
|
||||
augment class A {
|
||||
augment String get foo => '';
|
||||
}
|
||||
''');
|
||||
|
||||
configuration.withConstructors = false;
|
||||
checkElementText(library, r'''
|
||||
library
|
||||
reference: <testLibrary>
|
||||
fragments
|
||||
#F0 <testLibraryFragment>
|
||||
element: <testLibrary>
|
||||
classes
|
||||
#F1 class A (nameOffset:6) (firstTokenOffset:0) (offset:6)
|
||||
element: <testLibrary>::@class::A
|
||||
nextFragment: #F2
|
||||
fields
|
||||
#F3 isOriginGetterSetter foo (nameOffset:<null>) (firstTokenOffset:<null>) (offset:6)
|
||||
element: <testLibrary>::@class::A::@field::foo
|
||||
getters
|
||||
#F4 isCompleteDeclaration isOriginDeclaration foo (nameOffset:20) (firstTokenOffset:12) (offset:20)
|
||||
element: <testLibrary>::@class::A::@getter::foo
|
||||
nextFragment: #F5
|
||||
#F2 isAugmentation class A (nameOffset:47) (firstTokenOffset:33) (offset:47)
|
||||
element: <testLibrary>::@class::A
|
||||
previousFragment: #F1
|
||||
getters
|
||||
#F5 isAugmentation isCompleteDeclaration isOriginDeclaration foo (nameOffset:72) (firstTokenOffset:53) (offset:72)
|
||||
element: <testLibrary>::@class::A::@getter::foo
|
||||
previousFragment: #F4
|
||||
classes
|
||||
isSimplyBounded class A
|
||||
reference: <testLibrary>::@class::A
|
||||
firstFragment: #F1
|
||||
fields
|
||||
isOriginGetterSetter foo
|
||||
reference: <testLibrary>::@class::A::@field::foo
|
||||
firstFragment: #F3
|
||||
type: int
|
||||
getter: <testLibrary>::@class::A::@getter::foo
|
||||
getters
|
||||
isOriginDeclaration foo
|
||||
reference: <testLibrary>::@class::A::@getter::foo
|
||||
firstFragment: #F4
|
||||
returnType: int
|
||||
variable: <testLibrary>::@class::A::@field::foo
|
||||
''');
|
||||
}
|
||||
|
||||
test_getter_augmentation_chain_twoInSameDeclaration() async {
|
||||
var library = await buildLibrary(r'''
|
||||
class A {
|
||||
@@ -38878,6 +38933,51 @@ library
|
||||
''');
|
||||
}
|
||||
|
||||
test_method_augmentation_chain_returnType_void_int() async {
|
||||
var library = await buildLibrary(r'''
|
||||
class A {
|
||||
void foo() {}
|
||||
}
|
||||
|
||||
augment class A {
|
||||
augment int foo() => 0;
|
||||
}
|
||||
''');
|
||||
|
||||
configuration.withConstructors = false;
|
||||
checkElementText(library, r'''
|
||||
library
|
||||
reference: <testLibrary>
|
||||
fragments
|
||||
#F0 <testLibraryFragment>
|
||||
element: <testLibrary>
|
||||
classes
|
||||
#F1 class A (nameOffset:6) (firstTokenOffset:0) (offset:6)
|
||||
element: <testLibrary>::@class::A
|
||||
nextFragment: #F2
|
||||
methods
|
||||
#F3 isCompleteDeclaration isOriginDeclaration foo (nameOffset:17) (firstTokenOffset:12) (offset:17)
|
||||
element: <testLibrary>::@class::A::@method::foo
|
||||
nextFragment: #F4
|
||||
#F2 isAugmentation class A (nameOffset:43) (firstTokenOffset:29) (offset:43)
|
||||
element: <testLibrary>::@class::A
|
||||
previousFragment: #F1
|
||||
methods
|
||||
#F4 isAugmentation isCompleteDeclaration isOriginDeclaration foo (nameOffset:61) (firstTokenOffset:49) (offset:61)
|
||||
element: <testLibrary>::@class::A::@method::foo
|
||||
previousFragment: #F3
|
||||
classes
|
||||
isSimplyBounded class A
|
||||
reference: <testLibrary>::@class::A
|
||||
firstFragment: #F1
|
||||
methods
|
||||
isOriginDeclaration foo
|
||||
reference: <testLibrary>::@class::A::@method::foo
|
||||
firstFragment: #F3
|
||||
returnType: void
|
||||
''');
|
||||
}
|
||||
|
||||
test_method_augmentation_chain_twoDeclarations() async {
|
||||
var library = await buildLibrary(r'''
|
||||
class A {
|
||||
|
||||
@@ -1863,6 +1863,34 @@ library
|
||||
''');
|
||||
}
|
||||
|
||||
test_function_augmentation_chain_returnType_void_int() async {
|
||||
var library = await buildLibrary(r'''
|
||||
void foo() {}
|
||||
augment int foo() => 0;
|
||||
''');
|
||||
|
||||
configuration.withConstructors = false;
|
||||
checkElementText(library, r'''
|
||||
library
|
||||
reference: <testLibrary>
|
||||
fragments
|
||||
#F0 <testLibraryFragment>
|
||||
element: <testLibrary>
|
||||
functions
|
||||
#F1 isCompleteDeclaration isOriginDeclaration isStatic foo (nameOffset:5) (firstTokenOffset:0) (offset:5)
|
||||
element: <testLibrary>::@function::foo
|
||||
nextFragment: #F2
|
||||
#F2 isAugmentation isCompleteDeclaration isOriginDeclaration isStatic foo (nameOffset:26) (firstTokenOffset:14) (offset:26)
|
||||
element: <testLibrary>::@function::foo
|
||||
previousFragment: #F1
|
||||
functions
|
||||
isOriginDeclaration isStatic foo
|
||||
reference: <testLibrary>::@function::foo
|
||||
firstFragment: #F1
|
||||
returnType: void
|
||||
''');
|
||||
}
|
||||
|
||||
test_function_augmentation_chain_typeParameters_count_111() async {
|
||||
var library = await buildLibrary(r'''
|
||||
void foo<T>() {}
|
||||
|
||||
Reference in New Issue
Block a user