mirror of
https://github.com/vlang/v.git
synced 2023-08-10 21:13:21 +03:00
all: static type methods
This commit is contained in:
parent
aacdd61e67
commit
a9f8b5dadc
@ -105,7 +105,7 @@ fn (app App) gen_api_for_module_in_os(mod_name string, os_name string) string {
|
||||
for s in f.stmts {
|
||||
if s is ast.FnDecl {
|
||||
if s.is_pub {
|
||||
fn_signature := s.stringify(b.table, mod_name, map[string]string{})
|
||||
fn_signature := s.stringify_fn_decl(b.table, mod_name, map[string]string{})
|
||||
fn_mod := s.modname()
|
||||
if fn_mod == mod_name {
|
||||
fline := '${fn_mod}: ${fn_signature}'
|
||||
|
108
vlib/v/ast/ast.v
108
vlib/v/ast/ast.v
@ -505,40 +505,41 @@ pub mut:
|
||||
[minify]
|
||||
pub struct FnDecl {
|
||||
pub:
|
||||
name string // 'math.bits.normalize'
|
||||
short_name string // 'normalize'
|
||||
mod string // 'math.bits'
|
||||
is_deprecated bool
|
||||
is_pub bool
|
||||
is_variadic bool
|
||||
is_anon bool
|
||||
is_noreturn bool // true, when [noreturn] is used on a fn
|
||||
is_manualfree bool // true, when [manualfree] is used on a fn
|
||||
is_main bool // true for `fn main()`
|
||||
is_test bool // true for `fn test_abcde() {}`, false for `fn test_abc(x int) {}`, or for fns that do not start with test_
|
||||
is_conditional bool // true for `[if abc] fn abc(){}`
|
||||
is_exported bool // true for `[export: 'exact_C_name']`
|
||||
is_keep_alive bool // passed memory must not be freed (by GC) before function returns
|
||||
is_unsafe bool // true, when [unsafe] is used on a fn
|
||||
is_markused bool // true, when an explicit `[markused]` tag was put on a fn; `-skip-unused` will not remove that fn
|
||||
is_file_translated bool // true, when the file it resides in is `[translated]`
|
||||
receiver StructField // TODO this is not a struct field
|
||||
receiver_pos token.Pos // `(u User)` in `fn (u User) name()` position
|
||||
is_method bool
|
||||
method_type_pos token.Pos // `User` in ` fn (u User)` position
|
||||
method_idx int
|
||||
rec_mut bool // is receiver mutable
|
||||
rec_share ShareType
|
||||
language Language // V, C, JS
|
||||
file_mode Language // whether *the file*, where a function was a '.c.v', '.js.v' etc.
|
||||
no_body bool // just a definition `fn C.malloc()`
|
||||
is_builtin bool // this function is defined in builtin/strconv
|
||||
body_pos token.Pos // function bodys position
|
||||
file string
|
||||
generic_names []string
|
||||
is_direct_arr bool // direct array access
|
||||
attrs []Attr
|
||||
ctdefine_idx int = -1 // the index in fn.attrs of `[if xyz]`, when such attribute exists
|
||||
name string // 'math.bits.normalize'
|
||||
short_name string // 'normalize'
|
||||
mod string // 'math.bits'
|
||||
is_deprecated bool
|
||||
is_pub bool
|
||||
is_variadic bool
|
||||
is_anon bool
|
||||
is_noreturn bool // true, when [noreturn] is used on a fn
|
||||
is_manualfree bool // true, when [manualfree] is used on a fn
|
||||
is_main bool // true for `fn main()`
|
||||
is_test bool // true for `fn test_abcde() {}`, false for `fn test_abc(x int) {}`, or for fns that do not start with test_
|
||||
is_conditional bool // true for `[if abc] fn abc(){}`
|
||||
is_exported bool // true for `[export: 'exact_C_name']`
|
||||
is_keep_alive bool // passed memory must not be freed (by GC) before function returns
|
||||
is_unsafe bool // true, when [unsafe] is used on a fn
|
||||
is_markused bool // true, when an explicit `[markused]` tag was put on a fn; `-skip-unused` will not remove that fn
|
||||
is_file_translated bool // true, when the file it resides in is `[translated]`
|
||||
receiver StructField // TODO this is not a struct field
|
||||
receiver_pos token.Pos // `(u User)` in `fn (u User) name()` position
|
||||
is_method bool
|
||||
is_static_type_method bool // true for `fn Foo.bar() {}`
|
||||
method_type_pos token.Pos // `User` in ` fn (u User)` position
|
||||
method_idx int
|
||||
rec_mut bool // is receiver mutable
|
||||
rec_share ShareType
|
||||
language Language // V, C, JS
|
||||
file_mode Language // whether *the file*, where a function was a '.c.v', '.js.v' etc.
|
||||
no_body bool // just a definition `fn C.malloc()`
|
||||
is_builtin bool // this function is defined in builtin/strconv
|
||||
body_pos token.Pos // function bodys position
|
||||
file string
|
||||
generic_names []string
|
||||
is_direct_arr bool // direct array access
|
||||
attrs []Attr
|
||||
ctdefine_idx int = -1 // the index in fn.attrs of `[if xyz]`, when such attribute exists
|
||||
pub mut:
|
||||
idx int // index in an external container; can be used to refer to the function in a more efficient way, just by its integer index
|
||||
params []Param
|
||||
@ -578,25 +579,26 @@ pub fn (f &FnDecl) new_method_with_receiver_type(new_type Type) FnDecl {
|
||||
[minify]
|
||||
pub struct Fn {
|
||||
pub:
|
||||
is_variadic bool
|
||||
language Language
|
||||
is_pub bool
|
||||
is_ctor_new bool // `[use_new] fn JS.Array.prototype.constructor()`
|
||||
is_deprecated bool // `[deprecated] fn abc(){}`
|
||||
is_noreturn bool // `[noreturn] fn abc(){}`
|
||||
is_unsafe bool // `[unsafe] fn abc(){}`
|
||||
is_placeholder bool
|
||||
is_main bool // `fn main(){}`
|
||||
is_test bool // `fn test_abc(){}`
|
||||
is_keep_alive bool // passed memory must not be freed (by GC) before function returns
|
||||
is_method bool // true for `fn (x T) name()`, and for interface declarations (which are also for methods)
|
||||
no_body bool // a pure declaration like `fn abc(x int)`; used in .vh files, C./JS. fns.
|
||||
is_file_translated bool // true, when the file it resides in is `[translated]`
|
||||
mod string
|
||||
file string
|
||||
file_mode Language
|
||||
pos token.Pos
|
||||
return_type_pos token.Pos
|
||||
is_variadic bool
|
||||
language Language
|
||||
is_pub bool
|
||||
is_ctor_new bool // `[use_new] fn JS.Array.prototype.constructor()`
|
||||
is_deprecated bool // `[deprecated] fn abc(){}`
|
||||
is_noreturn bool // `[noreturn] fn abc(){}`
|
||||
is_unsafe bool // `[unsafe] fn abc(){}`
|
||||
is_placeholder bool
|
||||
is_main bool // `fn main(){}`
|
||||
is_test bool // `fn test_abc(){}`
|
||||
is_keep_alive bool // passed memory must not be freed (by GC) before function returns
|
||||
is_method bool // true for `fn (x T) name()`, and for interface declarations (which are also for methods)
|
||||
is_static_type_method bool // true for `fn Foo.bar() {}`
|
||||
no_body bool // a pure declaration like `fn abc(x int)`; used in .vh files, C./JS. fns.
|
||||
is_file_translated bool // true, when the file it resides in is `[translated]`
|
||||
mod string
|
||||
file string
|
||||
file_mode Language
|
||||
pos token.Pos
|
||||
return_type_pos token.Pos
|
||||
pub mut:
|
||||
return_type Type
|
||||
receiver_type Type // != 0, when .is_method == true
|
||||
|
@ -41,7 +41,7 @@ pub fn (node &CallExpr) fkey() string {
|
||||
}
|
||||
|
||||
// These methods are used only by vfmt, vdoc, and for debugging.
|
||||
pub fn (node &AnonFn) stringify(t &Table, cur_mod string, m2a map[string]string) string {
|
||||
pub fn (node &AnonFn) stringify_anon_decl(t &Table, cur_mod string, m2a map[string]string) string {
|
||||
mut f := strings.new_builder(30)
|
||||
f.write_string('fn ')
|
||||
if node.inherited_vars.len > 0 {
|
||||
@ -65,7 +65,7 @@ pub fn (node &AnonFn) stringify(t &Table, cur_mod string, m2a map[string]string)
|
||||
return f.str()
|
||||
}
|
||||
|
||||
pub fn (node &FnDecl) stringify(t &Table, cur_mod string, m2a map[string]string) string {
|
||||
pub fn (node &FnDecl) stringify_fn_decl(t &Table, cur_mod string, m2a map[string]string) string {
|
||||
mut f := strings.new_builder(30)
|
||||
if node.is_pub {
|
||||
f.write_string('pub ')
|
||||
@ -85,12 +85,19 @@ pub fn (node &FnDecl) stringify(t &Table, cur_mod string, m2a map[string]string)
|
||||
styp = styp.trim('&')
|
||||
}
|
||||
f.write_string(styp + ') ')
|
||||
} else if node.is_static_type_method {
|
||||
mut styp := util.no_cur_mod(t.type_to_code(node.receiver.typ.clear_flag(.shared_f)),
|
||||
cur_mod)
|
||||
f.write_string(styp + '.')
|
||||
}
|
||||
name := if !node.is_method && node.language == .v {
|
||||
mut name := if !node.is_method && node.language == .v {
|
||||
node.name.all_after_last('.')
|
||||
} else {
|
||||
node.name
|
||||
}
|
||||
if node.is_static_type_method {
|
||||
name = name.after('__static__')
|
||||
}
|
||||
f.write_string(name)
|
||||
if name in ['+', '-', '*', '/', '%', '<', '>', '==', '!=', '>=', '<='] {
|
||||
f.write_string(' ')
|
||||
@ -374,6 +381,9 @@ pub fn (x Expr) str() string {
|
||||
if x.name.contains('.') {
|
||||
return '${x.name}(${sargs})${propagate_suffix}'
|
||||
}
|
||||
if x.name.contains('__static__') {
|
||||
return '${x.mod}.${x.name}(${sargs})${propagate_suffix}1'
|
||||
}
|
||||
return '${x.mod}.${x.name}(${sargs})${propagate_suffix}'
|
||||
}
|
||||
CharLiteral {
|
||||
|
@ -590,7 +590,7 @@ pub fn (mut b Builder) print_warnings_and_errors() {
|
||||
for stmt in file.stmts {
|
||||
if stmt is ast.FnDecl {
|
||||
if stmt.name == fn_name {
|
||||
fheader := stmt.stringify(b.table, 'main', map[string]string{})
|
||||
fheader := stmt.stringify_fn_decl(b.table, 'main', map[string]string{})
|
||||
redefines << FunctionRedefinition{
|
||||
fpath: file.path
|
||||
fline: stmt.pos.line_nr
|
||||
|
@ -118,7 +118,7 @@ pub fn (mut d Doc) stmt_signature(stmt ast.Stmt) string {
|
||||
return 'module ${stmt.name}'
|
||||
}
|
||||
ast.FnDecl {
|
||||
return stmt.stringify(d.table, d.fmt.cur_mod, d.fmt.mod2alias)
|
||||
return stmt.stringify_fn_decl(d.table, d.fmt.cur_mod, d.fmt.mod2alias)
|
||||
}
|
||||
else {
|
||||
d.fmt.out = strings.new_builder(1000)
|
||||
|
@ -1007,7 +1007,7 @@ pub fn (mut f Fmt) enum_decl(node ast.EnumDecl) {
|
||||
|
||||
pub fn (mut f Fmt) fn_decl(node ast.FnDecl) {
|
||||
f.attrs(node.attrs)
|
||||
f.write(node.stringify(f.table, f.cur_mod, f.mod2alias)) // `Expr` instead of `ast.Expr` in mod ast
|
||||
f.write(node.stringify_fn_decl(f.table, f.cur_mod, f.mod2alias)) // `Expr` instead of `ast.Expr` in mod ast
|
||||
// Handle trailing comments after fn header declarations
|
||||
if node.no_body && node.end_comments.len > 0 {
|
||||
first_comment := node.end_comments[0]
|
||||
@ -1032,7 +1032,7 @@ pub fn (mut f Fmt) fn_decl(node ast.FnDecl) {
|
||||
}
|
||||
|
||||
pub fn (mut f Fmt) anon_fn(node ast.AnonFn) {
|
||||
f.write(node.stringify(f.table, f.cur_mod, f.mod2alias)) // `Expr` instead of `ast.Expr` in mod ast
|
||||
f.write(node.stringify_anon_decl(f.table, f.cur_mod, f.mod2alias)) // `Expr` instead of `ast.Expr` in mod ast
|
||||
f.fn_body(node.decl)
|
||||
}
|
||||
|
||||
@ -1305,7 +1305,7 @@ pub fn (mut f Fmt) interface_field(field ast.StructField) {
|
||||
|
||||
pub fn (mut f Fmt) interface_method(method ast.FnDecl) {
|
||||
f.write('\t')
|
||||
f.write(method.stringify(f.table, f.cur_mod, f.mod2alias).all_after_first('fn '))
|
||||
f.write(method.stringify_fn_decl(f.table, f.cur_mod, f.mod2alias).all_after_first('fn '))
|
||||
f.comments(method.comments, inline: true, has_nl: false, level: .indent)
|
||||
f.writeln('')
|
||||
f.comments(method.next_comments, inline: false, has_nl: true, level: .indent)
|
||||
@ -1790,6 +1790,7 @@ pub fn (mut f Fmt) call_expr(node ast.CallExpr) {
|
||||
for arg in node.args {
|
||||
f.comments(arg.comments)
|
||||
}
|
||||
|
||||
mut is_method_newline := false
|
||||
if node.is_method {
|
||||
if node.name in ['map', 'filter', 'all', 'any'] {
|
||||
@ -1826,7 +1827,11 @@ pub fn (mut f Fmt) call_expr(node ast.CallExpr) {
|
||||
} else {
|
||||
name := f.short_module(node.name)
|
||||
f.mark_import_as_used(name)
|
||||
f.write('${name}')
|
||||
if node.name.contains('__static__') {
|
||||
f.write(name.replace('__static__', '.').capitalize())
|
||||
} else {
|
||||
f.write(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if node.mod == '' && node.name == '' {
|
||||
|
@ -893,12 +893,12 @@ pub fn (mut f Gen) enum_decl(node ast.EnumDecl) {
|
||||
|
||||
pub fn (mut f Gen) fn_decl(node ast.FnDecl) {
|
||||
f.attrs(node.attrs)
|
||||
f.write(node.stringify(f.table, f.cur_mod, f.mod2alias).replace('fn ', 'func '))
|
||||
f.write(node.stringify_fn_decl(f.table, f.cur_mod, f.mod2alias).replace('fn ', 'func '))
|
||||
f.fn_body(node)
|
||||
}
|
||||
|
||||
pub fn (mut f Gen) anon_fn(node ast.AnonFn) {
|
||||
f.write(node.stringify(f.table, f.cur_mod, f.mod2alias)) // `Expr` instead of `ast.Expr` in mod ast
|
||||
f.write(node.stringify_fn_decl(f.table, f.cur_mod, f.mod2alias)) // `Expr` instead of `ast.Expr` in mod ast
|
||||
f.fn_body(node.decl)
|
||||
}
|
||||
|
||||
@ -1123,7 +1123,7 @@ pub fn (mut f Gen) interface_field(field ast.StructField) {
|
||||
|
||||
pub fn (mut f Gen) interface_method(method ast.FnDecl) {
|
||||
f.write('\t')
|
||||
f.write(method.stringify(f.table, f.cur_mod, f.mod2alias).after('fn '))
|
||||
f.write(method.stringify_fn_decl(f.table, f.cur_mod, f.mod2alias).after('fn '))
|
||||
f.writeln('')
|
||||
for param in method.params {
|
||||
f.mark_types_import_as_used(param.typ)
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2019-2023 Alexander Medvednikov. All rights reserved.
|
||||
// Use of this source code is governed by an MIT license
|
||||
// that can be found in the LICENSE file.
|
||||
// that ca be found in the LICENSE file.
|
||||
module parser
|
||||
|
||||
import v.ast
|
||||
@ -10,16 +10,22 @@ import os
|
||||
|
||||
fn (mut p Parser) call_expr(language ast.Language, mod string) ast.CallExpr {
|
||||
first_pos := p.tok.pos()
|
||||
mut name := if language == .js { p.check_js_name() } else { p.check_name() }
|
||||
mut is_static_type_method := language == .v && name[0].is_capital() && p.tok.kind == .dot
|
||||
if is_static_type_method {
|
||||
p.check(.dot)
|
||||
name = name.to_lower() + '__static__' + p.check_name()
|
||||
}
|
||||
mut fn_name := if language == .c {
|
||||
'C.${p.check_name()}'
|
||||
'C.${name}'
|
||||
} else if language == .js {
|
||||
'JS.${p.check_js_name()}'
|
||||
'JS.${name}'
|
||||
} else if language == .wasm {
|
||||
'WASM.${p.check_name()}'
|
||||
'WASM.${name}'
|
||||
} else if mod.len > 0 {
|
||||
'${mod}.${p.check_name()}'
|
||||
'${mod}.${name}'
|
||||
} else {
|
||||
p.check_name()
|
||||
name
|
||||
}
|
||||
if language != .v {
|
||||
p.check_for_impure_v(language, first_pos)
|
||||
@ -271,6 +277,7 @@ fn (mut p Parser) fn_decl() ast.FnDecl {
|
||||
language: language
|
||||
}
|
||||
mut is_method := false
|
||||
mut is_static_type_method := false
|
||||
mut params := []ast.Param{}
|
||||
if p.tok.kind == .lpar {
|
||||
is_method = true
|
||||
@ -288,7 +295,16 @@ fn (mut p Parser) fn_decl() ast.FnDecl {
|
||||
name_pos := p.tok.pos()
|
||||
if p.tok.kind == .name {
|
||||
// TODO high order fn
|
||||
name = if language == .js { p.check_js_name() } else { p.check_name() }
|
||||
is_static_type_method = p.tok.lit.len > 0 && p.tok.lit[0].is_capital()
|
||||
&& p.peek_tok.kind == .dot && language == .v // `fn Foo.bar() {}`
|
||||
if is_static_type_method {
|
||||
type_name := p.tok.lit // "Foo"
|
||||
rec.typ = p.parse_type() //_with_mut(false) // ast.Type(p.table.find_type_idx(name))
|
||||
p.check(.dot)
|
||||
name = type_name.to_lower() + '__static__' + p.check_name() // "foo__bar"
|
||||
} else {
|
||||
name = if language == .js { p.check_js_name() } else { p.check_name() }
|
||||
}
|
||||
if language == .v && !p.pref.translated && !p.is_translated && util.contains_capital(name)
|
||||
&& !p.builtin_mod {
|
||||
p.error_with_pos('function names cannot contain uppercase letters, use snake_case instead',
|
||||
@ -519,6 +535,8 @@ run them via `v file.v` instead',
|
||||
is_test: is_test
|
||||
is_keep_alive: is_keep_alive
|
||||
is_method: false
|
||||
is_static_type_method: is_static_type_method
|
||||
receiver_type: rec.typ // used only if is static type method
|
||||
is_file_translated: p.is_translated
|
||||
//
|
||||
attrs: p.attrs
|
||||
@ -588,6 +606,7 @@ run them via `v file.v` instead',
|
||||
generic_names: generic_names
|
||||
receiver_pos: rec.pos
|
||||
is_method: is_method
|
||||
is_static_type_method: is_static_type_method
|
||||
method_type_pos: rec.type_pos
|
||||
method_idx: type_sym_method_idx
|
||||
rec_mut: rec.is_mut
|
||||
|
@ -515,7 +515,7 @@ fn (mut p Parser) parse_any_type(language ast.Language, is_ptr bool, check_dot b
|
||||
name = 'C.${name}'
|
||||
} else if language == .js {
|
||||
name = 'JS.${name}'
|
||||
} else if p.peek_tok.kind == .dot && check_dot {
|
||||
} else if p.peek_tok.kind == .dot && check_dot && !name[0].is_capital() {
|
||||
// `module.Type`
|
||||
mut mod := name
|
||||
mut mod_pos := p.tok.pos()
|
||||
|
@ -1941,6 +1941,7 @@ fn (mut p Parser) note(s string) {
|
||||
}
|
||||
|
||||
fn (mut p Parser) error_with_pos(s string, pos token.Pos) ast.NodeError {
|
||||
// print_backtrace()
|
||||
mut kind := 'error:'
|
||||
if p.pref.fatal_errors {
|
||||
util.show_compiler_message(kind, pos: pos, file_path: p.file_name, message: s)
|
||||
@ -2592,6 +2593,7 @@ fn (mut p Parser) name_expr() ast.Expr {
|
||||
is_generic_call := p.is_generic_call()
|
||||
is_generic_cast := p.is_generic_cast()
|
||||
is_generic_struct_init := p.is_generic_struct_init()
|
||||
// mut is_static_type_method := false
|
||||
// p.warn('name expr $p.tok.lit $p.peek_tok.str()')
|
||||
same_line := p.tok.line_nr == p.peek_tok.line_nr
|
||||
// `(` must be on same line as name token otherwise it's a ParExpr
|
||||
@ -2673,6 +2675,7 @@ fn (mut p Parser) name_expr() ast.Expr {
|
||||
return node
|
||||
} else {
|
||||
// fn call
|
||||
// fn_call:
|
||||
if is_option {
|
||||
p.unexpected_with_pos(p.prev_tok.pos(),
|
||||
got: '${p.prev_tok}'
|
||||
@ -2745,8 +2748,14 @@ fn (mut p Parser) name_expr() ast.Expr {
|
||||
}
|
||||
}
|
||||
if p.peek_token(2).kind == .name && p.peek_token(3).kind == .lpar && !known_var {
|
||||
p.error_with_pos('the receiver of the method call must be an instantiated object, e.g. `foo.bar()`',
|
||||
p.tok.pos())
|
||||
if lit0_is_capital && p.peek_tok.kind == .dot && language == .v {
|
||||
// New static method call
|
||||
p.expr_mod = ''
|
||||
return p.call_expr(language, mod)
|
||||
} else {
|
||||
p.error_with_pos('${lit0_is_capital} the receiver of the method call must be an instantiated object, e.g. `foo.bar()`',
|
||||
p.tok.pos())
|
||||
}
|
||||
}
|
||||
// `Color.green`
|
||||
mut enum_name := p.check_name()
|
||||
|
@ -1,7 +1,21 @@
|
||||
vlib/v/parser/tests/method_call_receiver_err.vv:9:11: error: the receiver of the method call must be an instantiated object, e.g. `foo.bar()`
|
||||
vlib/v/parser/tests/method_call_receiver_err.vv:6:2: warning: unused variable: `s1`
|
||||
4 |
|
||||
5 | fn main() {
|
||||
6 | s1 := S1{}
|
||||
| ~~
|
||||
7 |
|
||||
8 | $for method in S1.methods {
|
||||
vlib/v/parser/tests/method_call_receiver_err.vv:8:7: warning: unused variable: `method`
|
||||
6 | s1 := S1{}
|
||||
7 |
|
||||
8 | $for method in S1.methods {
|
||||
| ~~~~~~
|
||||
9 | println(S1.method_hello('yo'))
|
||||
10 | }
|
||||
vlib/v/parser/tests/method_call_receiver_err.vv:9:11: error: unknown function: s1__static__method_hello
|
||||
7 |
|
||||
8 | $for method in S1.methods {
|
||||
9 | println(S1.method_hello('yo'))
|
||||
| ~~
|
||||
| ~~~~~~~~~~~~~~~~~~~~~
|
||||
10 | }
|
||||
11 | }
|
||||
|
@ -172,3 +172,31 @@ fn test_fn_return_fn() {
|
||||
f := ff()
|
||||
assert f() == 22
|
||||
}
|
||||
|
||||
// Test new static methods
|
||||
|
||||
struct Foo {
|
||||
x int
|
||||
}
|
||||
|
||||
struct Foo2 {
|
||||
x int
|
||||
}
|
||||
|
||||
fn (f Foo) normal_method() {
|
||||
}
|
||||
|
||||
fn Foo.static_method() int {
|
||||
return 7
|
||||
}
|
||||
|
||||
fn Foo2.static_method() int {
|
||||
return 8
|
||||
}
|
||||
|
||||
fn test_static_method() {
|
||||
x := Foo.static_method()
|
||||
assert x == 7
|
||||
x2 := Foo2.static_method()
|
||||
assert x2 == 8
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user