From 114fda5c6ce9061f6212d0bd98ebd30b1cca3f72 Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Mon, 30 Mar 2026 14:15:44 +0200 Subject: [PATCH] fix: preserve attribute call syntax in vfmt (#26769) * fix: preserve attribute call syntax in vfmt Fixes #26766 * fix: avoid decl-after-label in v2 parser --- vlib/v/ast/attr.v | 5 +- vlib/v/fmt/attrs.v | 118 ++++++++++++++++-- .../tests/attribute_call_syntax_expected.vv | 5 + .../fmt/tests/attribute_call_syntax_input.vv | 5 + .../v/fmt/tests/attribute_call_syntax_keep.vv | 13 ++ vlib/v/parser/attribute.v | 60 +++++---- vlib/v2/parser/parser.v | 6 +- 7 files changed, 177 insertions(+), 35 deletions(-) create mode 100644 vlib/v/fmt/tests/attribute_call_syntax_expected.vv create mode 100644 vlib/v/fmt/tests/attribute_call_syntax_input.vv create mode 100644 vlib/v/fmt/tests/attribute_call_syntax_keep.vv diff --git a/vlib/v/ast/attr.v b/vlib/v/ast/attr.v index 6762156a8..c79fed2d3 100644 --- a/vlib/v/ast/attr.v +++ b/vlib/v/ast/attr.v @@ -25,6 +25,9 @@ pub: ct_opt bool // true for [if user_defined_name?] pos token.Pos has_at bool // new syntax `@[attr]` + // original call-style metadata for `@[foo(...)]`, used by vfmt + call_name string + call_arg_name string pub mut: ct_expr Expr // .kind == comptime_define, for [if !name] ct_evaled bool // whether ct_skip has been evaluated already @@ -32,7 +35,7 @@ pub mut: } pub fn (a &Attr) debug() string { - return 'Attr{ name: "${a.name}", has_arg: ${a.has_arg}, arg: "${a.arg}", kind: ${a.kind}, ct_expr: ${a.ct_expr}, ct_opt: ${a.ct_opt}, ct_skip: ${a.ct_skip}}' + return 'Attr{ name: "${a.name}", has_arg: ${a.has_arg}, arg: "${a.arg}", kind: ${a.kind}, ct_expr: ${a.ct_expr}, ct_opt: ${a.ct_opt}, ct_skip: ${a.ct_skip}, call_name: "${a.call_name}", call_arg_name: "${a.call_arg_name}" }' } // str returns the string representation without square brackets diff --git a/vlib/v/fmt/attrs.v b/vlib/v/fmt/attrs.v index be275c248..83d6f4879 100644 --- a/vlib/v/fmt/attrs.v +++ b/vlib/v/fmt/attrs.v @@ -6,6 +6,14 @@ module fmt import v.ast pub fn (mut f Fmt) attrs(attrs []ast.Attr) { + if attrs_have_call_syntax(attrs) { + f.call_syntax_attrs(attrs) + return + } + f.legacy_attrs(attrs) +} + +fn (mut f Fmt) legacy_attrs(attrs []ast.Attr) { mut sorted_attrs := attrs.clone() // Sort the attributes. The ones with arguments come first sorted_attrs.sort_with_compare(fn (a &ast.Attr, b &ast.Attr) int { @@ -21,6 +29,24 @@ pub fn (mut f Fmt) attrs(attrs []ast.Attr) { } } +fn (mut f Fmt) call_syntax_attrs(attrs []ast.Attr) { + mut i := 0 + for i < attrs.len { + if attrs[i].call_name.len > 0 { + group, next_idx := attr_call_group(attrs, i) + f.writeln('@[${attr_call_group_str(group)}]') + i = next_idx + continue + } + mut j := i + for j < attrs.len && attrs[j].call_name.len == 0 { + j++ + } + f.legacy_attrs(attrs[i..j]) + i = j + } +} + @[params] pub struct AttrsOptions { pub: @@ -31,6 +57,22 @@ pub fn (mut f Fmt) single_line_attrs(attrs []ast.Attr, options AttrsOptions) { if attrs.len == 0 { return } + if attrs_have_call_syntax(attrs) { + if options.same_line { + f.write(' ') + } + f.write('@[') + f.write(single_line_attrs_text(attrs)) + f.write(']') + if !options.same_line { + f.writeln('') + } + return + } + f.legacy_single_line_attrs(attrs, options) +} + +fn (mut f Fmt) legacy_single_line_attrs(attrs []ast.Attr, options AttrsOptions) { mut sorted_attrs := attrs.clone() sorted_attrs.sort(a.name < b.name) if options.same_line { @@ -53,13 +95,75 @@ fn inline_attrs_len(attrs []ast.Attr) int { if attrs.len == 0 { return 0 } - mut n := 2 // ' ['.len - for i, attr in attrs { - if i > 0 { - n += 2 // '; '.len + return 3 + single_line_attrs_text(attrs).len // ' [' + ']'.len +} + +fn attrs_have_call_syntax(attrs []ast.Attr) bool { + return attrs.any(it.call_name.len > 0) +} + +fn single_line_attrs_text(attrs []ast.Attr) string { + if !attrs_have_call_syntax(attrs) { + mut sorted_attrs := attrs.clone() + sorted_attrs.sort(a.name < b.name) + mut parts := []string{cap: sorted_attrs.len} + for attr in sorted_attrs { + parts << '${attr}' + } + return parts.join('; ') + } + mut parts := []string{} + mut i := 0 + for i < attrs.len { + if attrs[i].call_name.len > 0 { + group, next_idx := attr_call_group(attrs, i) + parts << attr_call_group_str(group) + i = next_idx + continue + } + parts << '${attrs[i]}' + i++ + } + return parts.join('; ') +} + +fn attr_call_group(attrs []ast.Attr, start int) ([]ast.Attr, int) { + first := attrs[start] + mut end := start + 1 + for end < attrs.len && attrs[end].call_name == first.call_name + && attrs[end].pos.pos == first.pos.pos { + end++ + } + return attrs[start..end], end +} + +fn attr_call_group_str(attrs []ast.Attr) string { + if attrs.len == 0 { + return '' + } + mut args := []string{} + for attr in attrs { + if !attr.has_arg { + continue + } + mut arg := '' + if attr.call_arg_name.len > 0 { + arg += '${attr.call_arg_name}: ' } - n += '${attr}'.len + arg += attr_value_str(attr) + args << arg + } + if args.len == 0 { + return '${attrs[0].call_name}()' + } + return '${attrs[0].call_name}(${args.join(', ')})' +} + +fn attr_value_str(attr ast.Attr) string { + quote := if attr.quote == `"` { '"' } else { "'" } + return match attr.kind { + .plain, .number, .bool { attr.arg } + .string { '${quote}${attr.arg}${quote}' } + .comptime_define { 'if ${attr.arg}' } } - n++ // ']'.len - return n } diff --git a/vlib/v/fmt/tests/attribute_call_syntax_expected.vv b/vlib/v/fmt/tests/attribute_call_syntax_expected.vv new file mode 100644 index 000000000..1550dc407 --- /dev/null +++ b/vlib/v/fmt/tests/attribute_call_syntax_expected.vv @@ -0,0 +1,5 @@ +@[xml(name: 'foo', prefix: 'f', namespace: 'https://example.com/xmlns/foo')] +struct Foo {} + +@[deprecated(msg: 'use bar() instead', after: '2026-06-01')] +fn foo() {} diff --git a/vlib/v/fmt/tests/attribute_call_syntax_input.vv b/vlib/v/fmt/tests/attribute_call_syntax_input.vv new file mode 100644 index 000000000..1550dc407 --- /dev/null +++ b/vlib/v/fmt/tests/attribute_call_syntax_input.vv @@ -0,0 +1,5 @@ +@[xml(name: 'foo', prefix: 'f', namespace: 'https://example.com/xmlns/foo')] +struct Foo {} + +@[deprecated(msg: 'use bar() instead', after: '2026-06-01')] +fn foo() {} diff --git a/vlib/v/fmt/tests/attribute_call_syntax_keep.vv b/vlib/v/fmt/tests/attribute_call_syntax_keep.vv new file mode 100644 index 000000000..ec2c8ddaf --- /dev/null +++ b/vlib/v/fmt/tests/attribute_call_syntax_keep.vv @@ -0,0 +1,13 @@ +@[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() {} + +struct Config { + flag bool @[custom(flag: true, count: 2)] + value string @[xml(name: 'cfg'); raw] +} diff --git a/vlib/v/parser/attribute.v b/vlib/v/parser/attribute.v index 6a4de934f..cc0f896a5 100644 --- a/vlib/v/parser/attribute.v +++ b/vlib/v/parser/attribute.v @@ -42,6 +42,7 @@ fn (mut p Parser) parse_attr_call(name string, is_at bool, apos token.Pos) []ast mut base_kind := ast.AttrKind.plain mut base_arg := '' mut base_quote := u8(`'`) + mut base_arg_name := '' mut base_has_arg := false mut attrs := []ast.Attr{} mut has_base_arg := false @@ -66,16 +67,19 @@ fn (mut p Parser) parse_attr_call(name string, is_at bool, apos token.Pos) []ast base_arg = arg base_kind = kind base_quote = quote + base_arg_name = arg_name 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 + name: '${name}_${arg_name}' + has_arg: true + arg: arg + kind: kind + quote: quote + pos: apos.extend(p.prev_tok.pos()) + has_at: is_at + call_name: name + call_arg_name: arg_name } } } else if !has_base_arg { @@ -86,13 +90,14 @@ fn (mut p Parser) parse_attr_call(name string, is_at bool, apos token.Pos) []ast 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 + 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 + call_name: name } positional_arg_idx++ } @@ -104,13 +109,15 @@ fn (mut p Parser) parse_attr_call(name string, is_at bool, apos token.Pos) []ast } 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 + 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 + call_name: name + call_arg_name: base_arg_name } attrs.insert(0, base_attr) return attrs @@ -125,16 +132,19 @@ 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() + mut call_name := '' if p.tok.kind == .lpar { p.check(.lpar) p.check(.rpar) + call_name = 'unsafe' } return [ ast.Attr{ - name: 'unsafe' - kind: kind - pos: apos.extend(p.prev_tok.pos()) - has_at: is_at + name: 'unsafe' + kind: kind + pos: apos.extend(p.prev_tok.pos()) + has_at: is_at + call_name: call_name }, ] } diff --git a/vlib/v2/parser/parser.v b/vlib/v2/parser/parser.v index ad6d427d0..52de98a3d 100644 --- a/vlib/v2/parser/parser.v +++ b/vlib/v2/parser/parser.v @@ -68,14 +68,16 @@ pub fn (mut p Parser) parse_file(filename string, mut file_set token.FileSet) as if filename == '' { panic('parser.parse_file empty filename') } + mut src := '' + mut sw := time.StopWatch{} if !p.pref.verbose { unsafe { goto start_no_time } } - mut sw := time.new_stopwatch() + sw = time.new_stopwatch() start_no_time: - src := os.read_file(filename) or { p.error('error reading `' + filename + '`') } + src = os.read_file(filename) or { p.error('error reading `' + filename + '`') } p.init(filename, src, mut file_set) // start p.next() -- 2.39.5