From ebe3251a4eb2e4c8f30385add563df4fba65a2ac Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 26 Feb 2026 22:34:57 +0300 Subject: [PATCH] parser: new syntax for attributes (fixes #25502) --- doc/docs.md | 17 +- vlib/v/parser/assign.v | 6 +- vlib/v/parser/attribute.v | 195 +++++++++++++----- vlib/v/parser/tests/attribute_call_syntax.out | 0 vlib/v/parser/tests/attribute_call_syntax.vv | 18 ++ .../comptime_attribute_call_syntax_test.v | 69 +++++++ 6 files changed, 248 insertions(+), 57 deletions(-) create mode 100644 vlib/v/parser/tests/attribute_call_syntax.out create mode 100644 vlib/v/parser/tests/attribute_call_syntax.vv create mode 100644 vlib/v/tests/comptime/comptime_attribute_call_syntax_test.v diff --git a/doc/docs.md b/doc/docs.md index f8b3aca17..60787f12e 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -5956,6 +5956,8 @@ V has several attributes that modify the behavior of functions and structs. An attribute is a compiler instruction specified inside `[]` right before a function/struct/enum declaration and applies only to the following declaration. +Attributes with arguments support both `name: value` and call-style `name(value)` syntax. +Call-style attributes can also use named arguments. ```v // @[flag] enables Enum types to be used as bitfields @@ -6068,18 +6070,19 @@ Depending on the type and impact of the change, you may want to consult with the deprecating a function. -```v +```v nofmt // Calling this function will result in a deprecation warning - @[deprecated] -fn old_function() { -} +fn old_function() {} // It can also display a custom deprecation message - @[deprecated: 'use new_function() instead'] fn legacy_function() {} +// Equivalent call-style syntax: +@[deprecated('use new_function() instead')] +fn legacy_function_call_style() {} + // You can also specify a date, after which the function will be // considered deprecated. Before that date, calls to the function // will be compiler notices - you will see them, but the compilation @@ -6092,6 +6095,10 @@ fn legacy_function() {} @[deprecated: 'use new_function2() instead'] @[deprecated_after: '2021-05-27'] fn legacy_function2() {} + +// Equivalent call-style syntax: +@[deprecated(msg: 'use new_function2() instead', after: '2021-05-27')] +fn legacy_function2_call_style() {} ``` ```v globals diff --git a/vlib/v/parser/assign.v b/vlib/v/parser/assign.v index 3e4e94b24..f323a3d4b 100644 --- a/vlib/v/parser/assign.v +++ b/vlib/v/parser/assign.v @@ -300,7 +300,11 @@ fn (mut p Parser) partial_assign_stmt(left []ast.Expr) ast.Stmt { if p.tok.kind == .at && p.tok.line_nr == p.prev_tok.line_nr { p.check(.at) p.check(.lsbr) - attr = p.parse_attr(true) + attrs := p.parse_attr(true) + if attrs.len != 1 { + p.error_with_pos('assignment attributes support at most one argument', p.prev_tok.pos()) + } + attr = attrs[0] p.check(.rsbr) } pos.update_last_line(p.prev_tok.line_nr) diff --git a/vlib/v/parser/attribute.v b/vlib/v/parser/attribute.v index b0d7fe8a4..3b629ab2b 100644 --- a/vlib/v/parser/attribute.v +++ b/vlib/v/parser/attribute.v @@ -14,7 +14,109 @@ fn (p &Parser) current_attr_string_quote() u8 { return `'` } -fn (mut p Parser) parse_attr(is_at bool) ast.Attr { +fn (mut p Parser) parse_attr_arg(err_context string) (ast.AttrKind, string, u8) { + if p.tok.kind == .name { // `name: arg` + return ast.AttrKind.plain, p.check_name(), `'` + } else if p.tok.kind == .number { // `name: 123` + arg := p.tok.lit + p.next() + return ast.AttrKind.number, arg, `'` + } else if p.tok.kind == .string { // `name: 'arg'` + arg := p.tok.lit + quote := p.current_attr_string_quote() + p.next() + return ast.AttrKind.string, arg, quote + } else if p.tok.kind == .key_true || p.tok.kind == .key_false { // `name: true` + arg := p.tok.kind.str() + p.next() + return ast.AttrKind.bool, arg, `'` + } else if token.is_key(p.tok.lit) { // `name: keyword` + return ast.AttrKind.plain, p.check_name(), `'` + } + p.unexpected(additional_msg: 'an argument is expected${err_context}') + return ast.AttrKind.plain, '', `'` +} + +fn (mut p Parser) parse_attr_call(name string, is_at bool, apos token.Pos) []ast.Attr { + p.check(.lpar) + mut base_kind := ast.AttrKind.plain + mut base_arg := '' + mut base_quote := u8(`'`) + mut base_has_arg := false + mut attrs := []ast.Attr{} + mut has_base_arg := false + mut positional_arg_idx := 1 + for p.tok.kind !in [.rpar, .eof] { + mut is_named := false + mut arg_name := '' + if p.tok.kind == .name && p.peek_token(1).kind == .colon { + is_named = true + arg_name = p.tok.lit + p.next() + p.check(.colon) + } + kind, arg, quote := p.parse_attr_arg(' in `(...)`') + if is_named { + if name == 'deprecated' && arg_name == 'msg' { + if has_base_arg { + p.error_with_pos('duplicate `msg` argument for `@[deprecated(...)]` attribute', + apos.extend(p.prev_tok.pos())) + } + base_has_arg = true + base_arg = arg + base_kind = kind + base_quote = quote + has_base_arg = true + } else { + attrs << ast.Attr{ + name: '${name}_${arg_name}' + has_arg: true + arg: arg + kind: kind + quote: quote + pos: apos.extend(p.prev_tok.pos()) + has_at: is_at + } + } + } else if !has_base_arg { + base_has_arg = true + base_arg = arg + base_kind = kind + base_quote = quote + has_base_arg = true + } else { + attrs << ast.Attr{ + name: '${name}_${positional_arg_idx}' + has_arg: true + arg: arg + kind: kind + quote: quote + pos: apos.extend(p.prev_tok.pos()) + has_at: is_at + } + positional_arg_idx++ + } + if p.tok.kind == .comma { + p.next() + continue + } + break + } + p.check(.rpar) + base_attr := ast.Attr{ + name: name + has_arg: base_has_arg + arg: base_arg + kind: base_kind + quote: base_quote + pos: apos.extend(p.prev_tok.pos()) + has_at: is_at + } + attrs.insert(0, base_attr) + return attrs +} + +fn (mut p Parser) parse_attr(is_at bool) []ast.Attr { mut kind := ast.AttrKind.plain p.inside_attr_decl = true defer { @@ -23,12 +125,18 @@ fn (mut p Parser) parse_attr(is_at bool) ast.Attr { apos := if is_at { p.peek_token(-2).pos() } else { p.prev_tok.pos() } if p.tok.kind == .key_unsafe { p.next() - return ast.Attr{ - name: 'unsafe' - kind: kind - pos: apos.extend(p.tok.pos()) - has_at: is_at + if p.tok.kind == .lpar { + p.check(.lpar) + p.check(.rpar) } + return [ + ast.Attr{ + name: 'unsafe' + kind: kind + pos: apos.extend(p.prev_tok.pos()) + has_at: is_at + }, + ] } mut name := '' mut has_arg := false @@ -66,41 +174,24 @@ fn (mut p Parser) parse_attr(is_at bool) ast.Attr { if p.tok.kind == .colon { has_arg = true p.next() - if p.tok.kind == .name { // `name: arg` - kind = .plain - arg = p.check_name() - } else if p.tok.kind == .number { // `name: 123` - kind = .number - arg = p.tok.lit - p.next() - } else if p.tok.kind == .string { // `name: 'arg'` - kind = .string - arg = p.tok.lit - quote = p.current_attr_string_quote() - p.next() - } else if p.tok.kind == .key_true || p.tok.kind == .key_false { // `name: true` - kind = .bool - arg = p.tok.kind.str() - p.next() - } else if token.is_key(p.tok.lit) { // // `name: keyword` - kind = .plain - arg = p.check_name() - } else { - p.unexpected(additional_msg: 'an argument is expected after `:`') - } + kind, arg, quote = p.parse_attr_arg(' after `:`') + } else if p.tok.kind == .lpar { + return p.parse_attr_call(name, is_at, apos) } } - return ast.Attr{ - name: name - has_arg: has_arg - arg: arg - kind: kind - quote: quote - ct_expr: comptime_cond - ct_opt: comptime_cond_opt - pos: apos.extend(p.tok.pos()) - has_at: is_at - } + return [ + ast.Attr{ + name: name + has_arg: has_arg + arg: arg + kind: kind + quote: quote + ct_expr: comptime_cond + ct_opt: comptime_cond_opt + pos: apos.extend(p.tok.pos()) + has_at: is_at + }, + ] } fn (mut p Parser) is_attributes() bool { @@ -145,21 +236,23 @@ fn (mut p Parser) attributes() { mut has_ctdefine := false for p.tok.kind != .rsbr { attr_start_pos := p.tok.pos() - attr := p.parse_attr(is_at) - if p.attrs.contains(attr.name) && attr.name != 'wasm_export' { - p.error_with_pos('duplicate attribute `${attr.name}`', attr_start_pos.extend(p.prev_tok.pos())) - return - } - if attr.kind == .comptime_define { - if has_ctdefine { - p.error_with_pos('only one `[if flag]` may be applied at a time `${attr.name}`', - attr_start_pos.extend(p.prev_tok.pos())) + attrs := p.parse_attr(is_at) + for attr in attrs { + if p.attrs.contains(attr.name) && attr.name != 'wasm_export' { + p.error_with_pos('duplicate attribute `${attr.name}`', attr_start_pos.extend(p.prev_tok.pos())) return - } else { - has_ctdefine = true } + if attr.kind == .comptime_define { + if has_ctdefine { + p.error_with_pos('only one `[if flag]` may be applied at a time `${attr.name}`', + attr_start_pos.extend(p.prev_tok.pos())) + return + } else { + has_ctdefine = true + } + } + p.attrs << attr } - p.attrs << attr if p.tok.kind != .semicolon { if p.tok.kind == .rsbr { p.next() diff --git a/vlib/v/parser/tests/attribute_call_syntax.out b/vlib/v/parser/tests/attribute_call_syntax.out new file mode 100644 index 000000000..e69de29bb diff --git a/vlib/v/parser/tests/attribute_call_syntax.vv b/vlib/v/parser/tests/attribute_call_syntax.vv new file mode 100644 index 000000000..98c1dc8b3 --- /dev/null +++ b/vlib/v/parser/tests/attribute_call_syntax.vv @@ -0,0 +1,18 @@ +// vfmt off +@[unsafe()] +fn allow_unsafe_parentheses() {} + +@[deprecated('use new_fn instead')] +fn old_positional() {} + +@[deprecated(msg: 'use new_fn instead', after: '2999-10-10')] +fn old_named() {} + +@[deprecated('use new_fn instead', after: '2999-10-10')] +fn old_mixed() {} + +@[custom(flag: true, count: 2)] +fn custom_named_args() {} +// vfmt on + +fn new_fn() {} diff --git a/vlib/v/tests/comptime/comptime_attribute_call_syntax_test.v b/vlib/v/tests/comptime/comptime_attribute_call_syntax_test.v new file mode 100644 index 000000000..53c3fc90a --- /dev/null +++ b/vlib/v/tests/comptime/comptime_attribute_call_syntax_test.v @@ -0,0 +1,69 @@ +// vfmt off +@[deprecated('use NewPositional instead')] +struct OldPositional {} + +@[deprecated(msg: 'use NewNamed instead', after: '2999-01-01')] +struct OldNamed {} + +@[deprecated('use NewMixed instead', after: '2999-01-02')] +struct OldMixed {} + +@[custom(flag: true, count: 2)] +struct CustomNamed {} +// vfmt on + +fn test_attribute_call_syntax_positional_and_named_args() { + mut positional_msg := '' + $for attr in OldPositional.attributes { + if attr.name == 'deprecated' { + positional_msg = attr.arg + } + } + assert positional_msg == 'use NewPositional instead' + + mut named_msg := '' + mut named_after := '' + $for attr in OldNamed.attributes { + if attr.name == 'deprecated' { + named_msg = attr.arg + } + if attr.name == 'deprecated_after' { + named_after = attr.arg + } + } + assert named_msg == 'use NewNamed instead' + assert named_after == '2999-01-01' + + mut mixed_msg := '' + mut mixed_after := '' + $for attr in OldMixed.attributes { + if attr.name == 'deprecated' { + mixed_msg = attr.arg + } + if attr.name == 'deprecated_after' { + mixed_after = attr.arg + } + } + assert mixed_msg == 'use NewMixed instead' + assert mixed_after == '2999-01-02' +} + +fn test_attribute_call_syntax_generic_named_args() { + mut has_custom := false + mut custom_flag := '' + mut custom_count := '' + $for attr in CustomNamed.attributes { + if attr.name == 'custom' { + has_custom = true + } + if attr.name == 'custom_flag' { + custom_flag = attr.arg + } + if attr.name == 'custom_count' { + custom_count = attr.arg + } + } + assert has_custom + assert custom_flag == 'true' + assert custom_count == '2' +} -- 2.39.5