From 4e71138489dcda3860faa7b93f7af231a491a255 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Thu, 26 Feb 2026 20:57:29 +0300 Subject: [PATCH] comptime: fix struct comptime remove attrs' quotation mark (fixes #24186) --- vlib/encoding/binary/serialize.v | 16 ++++++- vlib/flag/flag_to.v | 13 +++++- vlib/v/ast/attr.v | 4 +- vlib/v/gen/c/comptime.v | 15 ++++--- vlib/v/parser/attribute.v | 15 +++++++ .../comptime_field_attrs_quotes_test.v | 13 ++++++ vlib/x/json2/attr_utils.v | 43 +++++++++++++++++++ vlib/x/json2/decode.v | 22 ++++++---- vlib/x/json2/encode.v | 9 ++-- 9 files changed, 127 insertions(+), 23 deletions(-) create mode 100644 vlib/v/tests/comptime/comptime_field_attrs_quotes_test.v create mode 100644 vlib/x/json2/attr_utils.v diff --git a/vlib/encoding/binary/serialize.v b/vlib/encoding/binary/serialize.v index 89088959b..0677ec7fa 100644 --- a/vlib/encoding/binary/serialize.v +++ b/vlib/encoding/binary/serialize.v @@ -39,6 +39,18 @@ pub fn encode_binary[T](obj T, config EncodeConfig) ![]u8 { return s.b } +@[inline] +fn normalize_attr_arg(value string) string { + mut normalized := value.trim_space() + if normalized.len > 1 { + if (normalized[0] == `'` && normalized[normalized.len - 1] == `'`) + || (normalized[0] == `"` && normalized[normalized.len - 1] == `"`) { + normalized = normalized[1..normalized.len - 1] + } + } + return normalized +} + fn encode_struct[T](mut s EncodeState, obj T) ! { $for field in T.fields { mut is_skip := false @@ -48,7 +60,7 @@ fn encode_struct[T](mut s EncodeState, obj T) ! { match f[0].trim_space() { 'serialize' { // @[serialize:'-'] - if f[1].trim_space() == '-' { + if normalize_attr_arg(f[1]) == '-' { is_skip = true } } @@ -235,7 +247,7 @@ fn decode_struct[T](mut s DecodeState, _ T) !T { match f[0].trim_space() { 'serialize' { // @[serialize:'-'] - if f[1].trim_space() == '-' { + if normalize_attr_arg(f[1]) == '-' { is_skip = true } } diff --git a/vlib/flag/flag_to.v b/vlib/flag/flag_to.v index 563fa3984..ba271e2b8 100644 --- a/vlib/flag/flag_to.v +++ b/vlib/flag/flag_to.v @@ -160,6 +160,17 @@ fn (fm FlagMapper) dbg_match(flag_ctx FlagContext, field StructField, arg string return '${struct_name}.${field.name}/${field.short}${extra} in ${flag_ctx.raw}/${flag_ctx.name} = `${arg}`' } +fn normalize_attr_value(value string) string { + trimmed := value.trim_space() + if trimmed.len > 1 { + if (trimmed[0] == `'` && trimmed[trimmed.len - 1] == `'`) + || (trimmed[0] == `"` && trimmed[trimmed.len - 1] == `"`) { + return trimmed[1..trimmed.len - 1] + } + } + return trimmed +} + fn (fm FlagMapper) get_struct_info[T]() !StructInfo { mut struct_fields := map[string]StructField{} mut struct_attrs := map[string]string{} @@ -184,7 +195,7 @@ fn (fm FlagMapper) get_struct_info[T]() !StructInfo { trace_println('\tattribute: "${attr}"') if attr.contains(':') { split := attr.split(':') - attrs[split[0].trim_space()] = split[1].trim(' ') + attrs[split[0].trim_space()] = normalize_attr_value(split[1]) } else { attrs[attr.trim(' ')] = 'true' } diff --git a/vlib/v/ast/attr.v b/vlib/v/ast/attr.v index 445651994..6762156a8 100644 --- a/vlib/v/ast/attr.v +++ b/vlib/v/ast/attr.v @@ -21,6 +21,7 @@ pub: has_arg bool arg string // [name: arg] kind AttrKind + quote u8 = `'` // quote for .string attrs: `"` or `'` ct_opt bool // true for [if user_defined_name?] pos token.Pos has_at bool // new syntax `@[attr]` @@ -37,6 +38,7 @@ pub fn (a &Attr) debug() string { // str returns the string representation without square brackets pub fn (a &Attr) str() string { mut s := '' + quote := if a.quote == `"` { '"' } else { "'" } mut arg := if a.has_arg { s += '${a.name}: ' a.arg @@ -45,7 +47,7 @@ pub fn (a &Attr) str() string { } s += match a.kind { .plain, .number, .bool { arg } - .string { "'${arg}'" } + .string { '${quote}${arg}${quote}' } .comptime_define { 'if ${arg}' } } return s diff --git a/vlib/v/gen/c/comptime.v b/vlib/v/gen/c/comptime.v index c24d7ce05..9a8157560 100644 --- a/vlib/v/gen/c/comptime.v +++ b/vlib/v/gen/c/comptime.v @@ -294,15 +294,16 @@ fn (mut g Gen) comptime_call(mut node ast.ComptimeCall) { fn cgen_attrs(attrs []ast.Attr) []string { mut res := []string{cap: attrs.len} for attr in attrs { - // we currently don't quote 'arg' (otherwise we could just use `s := attr.str()`) mut s := attr.name - if attr.arg.len > 0 { - s += ': ${attr.arg}' - } - if attr.kind == .string { - s = escape_quotes(s) + if attr.has_arg { + mut arg := attr.arg + if attr.kind == .string { + quote := if attr.quote == `"` { '"' } else { "'" } + arg = '${quote}${arg}${quote}' + } + s += ': ${arg}' } - res << '_S("${s}")' + res << '_S("${escape_quotes(s)}")' } return res } diff --git a/vlib/v/parser/attribute.v b/vlib/v/parser/attribute.v index cc97c4db6..b0d7fe8a4 100644 --- a/vlib/v/parser/attribute.v +++ b/vlib/v/parser/attribute.v @@ -3,6 +3,17 @@ module parser import v.ast import v.token +@[inline] +fn (p &Parser) current_attr_string_quote() u8 { + if p.tok.kind == .string && p.tok.pos >= 0 && p.tok.pos < p.scanner.text.len { + quote := p.scanner.text[p.tok.pos] + if quote in [`'`, `"`] { + return quote + } + } + return `'` +} + fn (mut p Parser) parse_attr(is_at bool) ast.Attr { mut kind := ast.AttrKind.plain p.inside_attr_decl = true @@ -22,6 +33,7 @@ fn (mut p Parser) parse_attr(is_at bool) ast.Attr { mut name := '' mut has_arg := false mut arg := '' + mut quote := u8(`'`) mut comptime_cond := ast.empty_expr mut comptime_cond_opt := false if p.tok.kind == .key_if { @@ -40,6 +52,7 @@ fn (mut p Parser) parse_attr(is_at bool) ast.Attr { name = comptime_cond.str() } else if p.tok.kind == .string { name = p.tok.lit + quote = p.current_attr_string_quote() kind = .string p.next() } else { @@ -63,6 +76,7 @@ fn (mut p Parser) parse_attr(is_at bool) ast.Attr { } 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 @@ -81,6 +95,7 @@ fn (mut p Parser) parse_attr(is_at bool) ast.Attr { 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()) diff --git a/vlib/v/tests/comptime/comptime_field_attrs_quotes_test.v b/vlib/v/tests/comptime/comptime_field_attrs_quotes_test.v new file mode 100644 index 000000000..d23578f35 --- /dev/null +++ b/vlib/v/tests/comptime/comptime_field_attrs_quotes_test.v @@ -0,0 +1,13 @@ +struct StructFieldAttrQuotes { + id1 string @[sql: "id"] + id2 string @[sql: 'id'] + id3 string @[sql: id] +} + +fn test_comptime_struct_field_attrs_keep_quotes() { + mut attrs := []string{} + $for field in StructFieldAttrQuotes.fields { + attrs << field.attrs[0] + } + assert attrs == ['sql: "id"', "sql: 'id'", 'sql: id'] +} diff --git a/vlib/x/json2/attr_utils.v b/vlib/x/json2/attr_utils.v new file mode 100644 index 000000000..b3b853694 --- /dev/null +++ b/vlib/x/json2/attr_utils.v @@ -0,0 +1,43 @@ +module json2 + +@[inline] +fn unquote_attr_value(value string) string { + mut unquoted := value.trim_space() + if unquoted.len > 1 { + if (unquoted[0] == `'` && unquoted[unquoted.len - 1] == `'`) + || (unquoted[0] == `"` && unquoted[unquoted.len - 1] == `"`) { + unquoted = unquoted[1..unquoted.len - 1] + } + } + return unquoted +} + +@[inline] +fn json_attr_value(attr string) ?string { + if !attr.starts_with('json:') { + return none + } + return unquote_attr_value(attr[5..]) +} + +fn json_attr_value_range(attr string) ?(int, int) { + if !attr.starts_with('json:') { + return none + } + mut start := 5 + for start < attr.len && attr[start] in [` `, `\t`, `\n`, `\r`] { + start++ + } + mut end := attr.len + for end > start && attr[end - 1] in [` `, `\t`, `\n`, `\r`] { + end-- + } + if end - start > 1 { + if (attr[start] == `'` && attr[end - 1] == `'`) + || (attr[start] == `"` && attr[end - 1] == `"`) { + start++ + end-- + } + } + return start, end +} diff --git a/vlib/x/json2/decode.v b/vlib/x/json2/decode.v index 05dd5f347..430c764fe 100644 --- a/vlib/x/json2/decode.v +++ b/vlib/x/json2/decode.v @@ -411,14 +411,18 @@ fn (mut decoder Decoder) decode_value[T](mut val T) ! { $for field in T.fields { mut json_name_str := field.name.str mut json_name_len := field.name.len + mut is_json_skip := false for attr in field.attrs { - if attr.starts_with('json:') { - if attr.len <= 6 { + if start, end := json_attr_value_range(attr) { + if end <= start { decoder.decode_error('`json` attribute must have an argument')! } - json_name_str = unsafe { attr.str + 6 } - json_name_len = attr.len - 6 + if end == start + 1 && attr[start] == `-` { + is_json_skip = true + } + json_name_str = unsafe { attr.str + start } + json_name_len = end - start break } continue @@ -430,7 +434,7 @@ fn (mut decoder Decoder) decode_value[T](mut val T) ! { json_name_ptr: voidptr(json_name_str) json_name_len: json_name_len is_omitempty: field.attrs.contains('omitempty') - is_skip: field.attrs.contains('skip') || field.attrs.contains('json: -') + is_skip: field.attrs.contains('skip') || is_json_skip is_required: field.attrs.contains('required') is_raw: field.attrs.contains('raw') }) @@ -875,9 +879,11 @@ fn (mut decoder Decoder) decode_enum[T](mut val T) ! { $for value in T.values { for attr in value.attrs { - if attr.starts_with('json: ') && attr[6..] == result { - val = value.value - return + if json_attr := json_attr_value(attr) { + if json_attr == result { + val = value.value + return + } } } if value.name == result { diff --git a/vlib/x/json2/encode.v b/vlib/x/json2/encode.v index c79c3a004..1390546d4 100644 --- a/vlib/x/json2/encode.v +++ b/vlib/x/json2/encode.v @@ -303,8 +303,8 @@ fn (mut encoder Encoder) encode_enum[T](val T) { $for member in T.values { if member.value == val { for attr in member.attrs { - if attr.starts_with('json: ') { - attr_value = attr[6..] + if json_attr := json_attr_value(attr) { + attr_value = json_attr } } } @@ -379,11 +379,12 @@ fn (mut encoder Encoder) cached_field_infos[T]() []EncoderFieldInfo { else {} } if attr.starts_with('json:') { - if attr == 'json: -' { + json_attr := json_attr_value(attr) or { continue } + if json_attr == '-' { is_skip = true break } - key_name = attr[6..] + key_name = json_attr } } field_infos << EncoderFieldInfo{ -- 2.39.5