From 4ac7f0124c5a81faa21b2bdef119ef731481a17a Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 15 Apr 2026 05:49:11 +0300 Subject: [PATCH] checker: fix using only C functions from a module making it appear unused (fixes #26232) --- vlib/v/builder/builder.v | 125 +++++++++++++++++- .../modules/c_fn_import_marks_module_used.out | 1 + .../cmod/cmod.c.v | 5 + .../c_fn_import_marks_module_used/main.v | 7 + vlib/v/parser/parser.v | 5 +- 5 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 vlib/v/checker/tests/modules/c_fn_import_marks_module_used.out create mode 100644 vlib/v/checker/tests/modules/c_fn_import_marks_module_used/cmod/cmod.c.v create mode 100644 vlib/v/checker/tests/modules/c_fn_import_marks_module_used/main.v diff --git a/vlib/v/builder/builder.v b/vlib/v/builder/builder.v index 9c4f1f4c7..5f7f7a25b 100644 --- a/vlib/v/builder/builder.v +++ b/vlib/v/builder/builder.v @@ -6,6 +6,7 @@ import v.pref import v.errors import v.util import v.ast +import v.ast.walker import v.vmod import v.checker import v.transformer @@ -59,6 +60,20 @@ pub mut: disable_flto bool } +struct CFunctionCallCollector { +mut: + names map[string]bool +} + +fn (mut c CFunctionCallCollector) visit(node &ast.Node) ! { + if node is ast.Expr && node is ast.CallExpr { + call := node as ast.CallExpr + if call.name.starts_with('C.') { + c.names[call.name] = true + } + } +} + pub fn new_builder(pref_ &pref.Preferences) Builder { rdir := os.real_path(pref_.path) compiled_dir := if os.is_dir(rdir) { rdir } else { os.dir(rdir) } @@ -135,6 +150,8 @@ pub fn (mut b Builder) interpret_text(code string, v_files []string) ! { exit(1) } b.parse_imports() + b.check_unused_imports() + b.print_frontend_builder_errors() if b.should_stop_after_frontend_error() && b.has_frontend_errors() { exit(1) } @@ -162,6 +179,8 @@ pub fn (mut b Builder) front_stages(v_files []string) ! { } b.parse_imports() + b.check_unused_imports() + b.print_frontend_builder_errors() if b.should_stop_after_frontend_error() && b.has_frontend_errors() { exit(1) } @@ -405,6 +424,94 @@ pub fn (mut b Builder) resolve_deps() { } } +fn import_alias_for_mod(file &ast.File, mod string) ?string { + for import_m in file.imports { + if import_m.mod == mod { + return import_m.alias + } + } + return none +} + +fn register_used_import(mut file ast.File, alias string) { + if alias !in file.used_imports { + file.used_imports << alias + } +} + +fn (mut b Builder) collect_used_c_function_calls(file &ast.File) map[string]bool { + mut collector := CFunctionCallCollector{ + names: map[string]bool{} + } + walker.walk(mut collector, file) + return collector.names +} + +fn (mut b Builder) mark_imports_used_by_c_function_calls() { + for mut file in b.parsed_files { + used_c_calls := b.collect_used_c_function_calls(file) + if used_c_calls.len == 0 { + continue + } + for c_fn_name, _ in used_c_calls { + c_fn := b.table.find_fn(c_fn_name) or { continue } + alias := import_alias_for_mod(file, c_fn.mod) or { continue } + register_used_import(mut file, alias) + } + } +} + +fn (mut b Builder) add_unused_import_message(mut file ast.File, message string, pos token.Pos) { + file_path := if pos.file_idx < 0 { file.path } else { b.table.filelist[pos.file_idx] } + if b.pref.warns_are_errors { + err := errors.Error{ + file_path: file_path + pos: pos + reporter: .parser + message: message + } + file.errors << err + if b.pref.output_mode == .stdout && !b.pref.check_only { + util.show_compiler_message('error:', err.CompilerMessage) + } + return + } + if b.pref.skip_warnings { + return + } + wrn := errors.Warning{ + file_path: file_path + pos: pos + reporter: .parser + message: message + } + file.warnings << wrn + if b.pref.output_mode == .stdout && !b.pref.check_only { + util.show_compiler_message('warning:', wrn.CompilerMessage) + } +} + +fn (mut b Builder) check_unused_imports() { + if b.pref.is_repl || b.pref.is_fmt { + return + } + b.mark_imports_used_by_c_function_calls() + for mut file in b.parsed_files { + for import_m in file.imports { + alias := import_m.alias + mod := import_m.mod + if (alias.len == 1 && alias[0] == `_`) || alias in file.used_imports + || alias in file.auto_imports { + continue + } + mod_alias := if alias == mod { alias } else { '${alias} (${mod})' } + b.add_unused_import_message(mut file, + "module '${mod_alias}' is imported but never used. Use `import ${mod_alias} as _`, to silence this warning, or just remove the unused import line", + import_m.mod_pos) + } + } +} + // graph of all imported modules pub fn (b &Builder) import_graph() &depgraph.DepGraph { builtins := util.builtin_module_parts.clone() @@ -813,11 +920,6 @@ struct FunctionRedefinition { } pub fn (b &Builder) error_with_pos(s string, fpath string, pos token.Pos) errors.Error { - if !b.pref.check_only { - util.show_compiler_message('builder error:', pos: pos, file_path: fpath, message: s) - exit(1) - } - return errors.Error{ file_path: fpath pos: pos @@ -826,6 +928,19 @@ pub fn (b &Builder) error_with_pos(s string, fpath string, pos token.Pos) errors } } +fn (b &Builder) print_frontend_builder_errors() { + if b.pref.check_only || b.pref.output_mode != .stdout { + return + } + for file in b.parsed_files { + for err in file.errors { + if err.reporter == .builder { + util.show_compiler_message('builder error:', err.CompilerMessage) + } + } + } +} + @[noreturn] pub fn verror(s string) { util.verror('builder error', s) diff --git a/vlib/v/checker/tests/modules/c_fn_import_marks_module_used.out b/vlib/v/checker/tests/modules/c_fn_import_marks_module_used.out new file mode 100644 index 000000000..00750edc0 --- /dev/null +++ b/vlib/v/checker/tests/modules/c_fn_import_marks_module_used.out @@ -0,0 +1 @@ +3 diff --git a/vlib/v/checker/tests/modules/c_fn_import_marks_module_used/cmod/cmod.c.v b/vlib/v/checker/tests/modules/c_fn_import_marks_module_used/cmod/cmod.c.v new file mode 100644 index 000000000..e3a1ff041 --- /dev/null +++ b/vlib/v/checker/tests/modules/c_fn_import_marks_module_used/cmod/cmod.c.v @@ -0,0 +1,5 @@ +module cmod + +#include + +fn C.strlen(s &char) usize diff --git a/vlib/v/checker/tests/modules/c_fn_import_marks_module_used/main.v b/vlib/v/checker/tests/modules/c_fn_import_marks_module_used/main.v new file mode 100644 index 000000000..478816af1 --- /dev/null +++ b/vlib/v/checker/tests/modules/c_fn_import_marks_module_used/main.v @@ -0,0 +1,7 @@ +module main + +import cmod + +fn main() { + println(C.strlen(c'abc')) +} diff --git a/vlib/v/parser/parser.v b/vlib/v/parser/parser.v index f0d2ae6c8..c4e161887 100644 --- a/vlib/v/parser/parser.v +++ b/vlib/v/parser/parser.v @@ -357,9 +357,8 @@ pub fn (mut p Parser) parse() &ast.File { } for { if p.tok.kind == .eof { - if !p.is_vls_skip_file { - p.check_unused_imports() - } + // Imported module files are discovered after the initial parse pass, + // so unused import warnings are emitted later by the builder. break } stmt := p.top_stmt() -- 2.39.5