From 21c1aba56103052ce5f62293c51659b8185155db Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 25 Mar 2026 16:42:19 +0300 Subject: [PATCH] parser: fix $tmpl not locating template file for composed paths (fixes #24752) --- doc/docs.md | 2 + vlib/v/parser/comptime.v | 148 ++++++++++++++++-- .../comptime_call_tmpl_composed_path_test.v | 32 ++++ .../comptime_call_tmpl_composed_path_test.txt | 1 + 4 files changed, 170 insertions(+), 13 deletions(-) create mode 100644 vlib/v/tests/comptime/comptime_call_tmpl_composed_path_test.v create mode 100644 vlib/v/tests/comptime/templates/comptime_call_tmpl_composed_path_test.txt diff --git a/doc/docs.md b/doc/docs.md index b67ebef8a..24d6fde24 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -6767,6 +6767,8 @@ which could be used to obtain the file contents as `string` or `[]u8`. V has a simple template language for text and html templates, and they can easily be embedded via `$tmpl('path/to/template.txt')`: +the path can also come from a compile-time string expression like concatenation, +`os.path_separator`, or `os.join_path(...)`. ```v ignore fn build() string { diff --git a/vlib/v/parser/comptime.v b/vlib/v/parser/comptime.v index bd63fe871..089500caf 100644 --- a/vlib/v/parser/comptime.v +++ b/vlib/v/parser/comptime.v @@ -7,6 +7,7 @@ import os import v.ast import v.token import v.errors +import v.util const supported_comptime_calls = ['html', 'tmpl', 'env', 'embed_file', 'pkgconfig', 'compile_error', 'compile_warn', 'd', 'res'] @@ -140,6 +141,132 @@ fn (mut p Parser) hash() ast.HashStmt { const error_msg = 'only `\$tmpl()`, `\$env()`, `\$embed_file()`, `\$pkgconfig()`, `\$veb.html()`, `\$vweb.html()`, `\$compile_error()`, `\$compile_warn()`, `\$d()` and `\$res()` comptime functions are supported right now' +fn (p &Parser) resolve_tmpl_path_expr(expr ast.Expr) ?string { + return p.resolve_tmpl_path_expr_with_depth(expr, 0) +} + +fn (p &Parser) resolve_tmpl_path_expr_with_depth(expr ast.Expr, depth int) ?string { + if depth > 50 { + return none + } + match expr { + ast.AtExpr { + return expr.name + } + ast.CallExpr { + match expr.name { + 'os.join_path' { + if expr.args.len == 0 { + return '' + } + mut path := p.resolve_tmpl_path_expr_with_depth(expr.args[0].expr, + depth + 1)? + for arg in expr.args[1..] { + path = os.join_path_single(path, p.resolve_tmpl_path_expr_with_depth(arg.expr, + depth + 1)?) + } + return path + } + 'os.join_path_single' { + if expr.args.len != 2 { + return none + } + return os.join_path_single(p.resolve_tmpl_path_expr_with_depth(expr.args[0].expr, + depth + 1)?, p.resolve_tmpl_path_expr_with_depth(expr.args[1].expr, + depth + 1)?) + } + else { + return none + } + } + } + ast.Ident { + if var := p.scope.find_var(expr.name) { + return p.resolve_tmpl_path_expr_with_depth(var.expr, depth + 1) + } + if expr.name == 'os.path_separator' { + return os.path_separator + } + if imported_name := p.imported_symbols[expr.name] { + if const_field := p.table.global_scope.find_const(imported_name) { + return p.resolve_tmpl_path_expr_with_depth(const_field.expr, depth + 1) + } + } + if const_field := p.table.global_scope.find_const(expr.name) { + return p.resolve_tmpl_path_expr_with_depth(const_field.expr, depth + 1) + } + if const_field := p.table.global_scope.find_const('${expr.mod}.${expr.name}') { + return p.resolve_tmpl_path_expr_with_depth(const_field.expr, depth + 1) + } + if const_field := p.table.global_scope.find_const('${p.mod}.${expr.name}') { + return p.resolve_tmpl_path_expr_with_depth(const_field.expr, depth + 1) + } + return none + } + ast.InfixExpr { + if expr.op != .plus { + return none + } + return p.resolve_tmpl_path_expr_with_depth(expr.left, depth + 1)? + + p.resolve_tmpl_path_expr_with_depth(expr.right, depth + 1)? + } + ast.ParExpr { + return p.resolve_tmpl_path_expr_with_depth(expr.expr, depth + 1) + } + ast.SelectorExpr { + module_name := p.resolve_tmpl_path_module_name(expr.expr) or { return none } + if module_name == 'os' && expr.field_name == 'path_separator' { + return os.path_separator + } + if const_field := p.table.global_scope.find_const('${module_name}.${expr.field_name}') { + return p.resolve_tmpl_path_expr_with_depth(const_field.expr, depth + 1) + } + return none + } + ast.StringLiteral { + return expr.val + } + else { + return none + } + } +} + +fn (p &Parser) resolve_tmpl_path_module_name(expr ast.Expr) ?string { + match expr { + ast.Ident { + if module_name := p.imports[expr.name] { + return module_name + } + return expr.name + } + ast.SelectorExpr { + return '${p.resolve_tmpl_path_module_name(expr.expr)?}.${expr.field_name}' + } + else { + return none + } + } +} + +fn (mut p Parser) resolve_tmpl_pseudo_variables(path string, pos token.Pos) ?string { + mut resolved := path + source_path := os.real_path(p.scanner.file_path) + if resolved.contains('@VEXEROOT') { + resolved = resolved.replace('@VEXEROOT', p.pref.vroot) + } + if resolved.contains('@VMODROOT') { + resolved = util.resolve_vmodroot(resolved, source_path) or { + p.error_with_pos(err.msg(), pos) + return none + } + } + if resolved.contains('@DIR') { + resolved = resolved.replace('@DIR', os.dir(source_path)) + } + return resolved +} + fn (mut p Parser) comptime_call() ast.ComptimeCall { err_node := ast.ComptimeCall{ scope: unsafe { nil } @@ -264,18 +391,6 @@ fn (mut p Parser) comptime_call() ast.ComptimeCall { } has_string_arg := p.tok.kind == .string mut literal_string_param := if is_html && !has_string_arg { '' } else { p.tok.lit } - if p.tok.kind == .name { - if var := p.scope.find_var(p.tok.lit) { - if var.expr is ast.StringLiteral { - literal_string_param = var.expr.val - } - } else if var := p.table.global_scope.find_const(p.mod + '.' + p.tok.lit) { - if var.expr is ast.StringLiteral { - literal_string_param = var.expr.val - } - } - } - path_of_literal_string_param := literal_string_param.replace('/', os.path_separator) mut arg := ast.CallArg{} if is_html && !(has_string_arg || p.tok.kind == .rpar) { p.error('expecting `\$vweb.html()` for a default template path or `\$vweb.html("/path/to/template.html")`') @@ -284,6 +399,9 @@ fn (mut p Parser) comptime_call() ast.ComptimeCall { // $vweb.html() can have no arguments } else { arg_expr := p.expr(0) + if resolved_path := p.resolve_tmpl_path_expr(arg_expr) { + literal_string_param = resolved_path + } arg = ast.CallArg{ expr: arg_expr } @@ -322,7 +440,11 @@ fn (mut p Parser) comptime_call() ast.ComptimeCall { tmpl_path := if is_html && !has_string_arg { '${fn_path.last()}.html' } else { - path_of_literal_string_param + mut resolved_path := literal_string_param.replace('/', os.path_separator) + resolved_path = p.resolve_tmpl_pseudo_variables(resolved_path, arg_pos) or { + return err_node + } + resolved_path } // Looking next to the vweb program dir := os.dir(compiled_vfile_path) diff --git a/vlib/v/tests/comptime/comptime_call_tmpl_composed_path_test.v b/vlib/v/tests/comptime/comptime_call_tmpl_composed_path_test.v new file mode 100644 index 000000000..31868dbb9 --- /dev/null +++ b/vlib/v/tests/comptime/comptime_call_tmpl_composed_path_test.v @@ -0,0 +1,32 @@ +import os + +const tmpl_dir_name = 'templates' +const tmpl_file_name = 'comptime_call_tmpl_composed_path_test.txt' +const tmpl_plus_path = tmpl_dir_name + '/' + tmpl_file_name +const tmpl_separator_path = tmpl_dir_name + os.path_separator + tmpl_file_name +const tmpl_joined_path = os.join_path(tmpl_dir_name, tmpl_file_name) + +fn test_comptime_tmpl_resolves_plus_path_const() { + value := 'plus' + rendered := $tmpl(tmpl_plus_path) + assert rendered.contains(value) +} + +fn test_comptime_tmpl_resolves_path_separator_const() { + value := 'separator' + rendered := $tmpl(tmpl_separator_path) + assert rendered.contains(value) +} + +fn test_comptime_tmpl_resolves_join_path_const() { + value := 'joined const' + rendered := $tmpl(tmpl_joined_path) + assert rendered.contains(value) +} + +fn test_comptime_tmpl_resolves_join_path_variable() { + value := 'joined var' + tmpl_path := os.join_path(tmpl_dir_name, tmpl_file_name) + rendered := $tmpl(tmpl_path) + assert rendered.contains(value) +} diff --git a/vlib/v/tests/comptime/templates/comptime_call_tmpl_composed_path_test.txt b/vlib/v/tests/comptime/templates/comptime_call_tmpl_composed_path_test.txt new file mode 100644 index 000000000..b1ead3d2d --- /dev/null +++ b/vlib/v/tests/comptime/templates/comptime_call_tmpl_composed_path_test.txt @@ -0,0 +1 @@ +@value -- 2.39.5