From af74f6ece50691bb2b62f1c17eb8fede5e37841c Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Wed, 25 Mar 2026 16:42:25 +0300 Subject: [PATCH] checker: add declaration errors for unused functions and constants (fixes #19079) --- doc/docs.md | 2 + vlib/v/ast/table.v | 54 +++---- vlib/v/checker/assign.v | 2 + vlib/v/checker/checker.v | 17 +++ vlib/v/checker/comptime.v | 1 + vlib/v/checker/fn.v | 8 + vlib/v/checker/infix.v | 14 ++ vlib/v/checker/tests/auto_ref_voidptr.out | 7 + .../function_stored_in_global.run.out | 7 + .../modules/unused_private_declarations.out | 14 ++ .../unused_private_declarations/main.v | 32 ++++ .../checker/tests/return_working_comp_if.out | 7 + .../tests/return_working_comp_if_nested.out | 7 + .../checker/tests/return_working_if_match.out | 7 + .../checker/tests/return_working_match_if.out | 7 + .../v/checker/tests/return_working_nested.out | 7 + .../v/checker/tests/return_working_simple.out | 7 + .../v/checker/tests/return_working_two_if.out | 7 + .../v/checker/tests/return_working_unsafe.out | 7 + .../tests/unused_private_declarations.out | 14 ++ .../tests/unused_private_declarations.vv | 32 ++++ .../tests/use_deprecated_function_warning.out | 7 + vlib/v/checker/tests/var_duplicate_const.out | 5 + vlib/v/checker/unused_declarations.v | 139 ++++++++++++++++++ vlib/v/checker/used_features.v | 2 + 25 files changed, 388 insertions(+), 25 deletions(-) create mode 100644 vlib/v/checker/tests/modules/unused_private_declarations.out create mode 100644 vlib/v/checker/tests/modules/unused_private_declarations/main.v create mode 100644 vlib/v/checker/tests/unused_private_declarations.out create mode 100644 vlib/v/checker/tests/unused_private_declarations.vv create mode 100644 vlib/v/checker/unused_declarations.v diff --git a/doc/docs.md b/doc/docs.md index 508e2318b..7bbd403d3 100644 --- a/doc/docs.md +++ b/doc/docs.md @@ -499,6 +499,8 @@ In development mode the compiler will warn you that you haven't used the variabl (you'll get an "unused variable" warning). In production mode (enabled by passing the `-prod` flag to v – `v -prod foo.v`) it will not compile at all (like in Go). +The same warning or error also applies to private top-level functions and constants +in the `main` module when they are never referenced. ```v fn main() { a := 10 diff --git a/vlib/v/ast/table.v b/vlib/v/ast/table.v index 113f4bab9..3a3bd23ff 100644 --- a/vlib/v/ast/table.v +++ b/vlib/v/ast/table.v @@ -11,31 +11,33 @@ import v.token @[heap; minify] pub struct UsedFeatures { pub mut: - dump bool // filled in by markused - anon_fn bool // fn () { } - auto_str bool // auto str fns - auto_str_ptr bool // auto str fns for ptr type - auto_str_arr bool // auto str fns for array - arr_prepend bool // arr.prepend() - arr_insert bool // arr.insert() - arr_first bool // arr.first() - arr_last bool // arr.last() - arr_pop_left bool // arr.pop_left() - arr_pop bool // arr.pop() - arr_delete bool // arr.delete() - arr_reverse bool // arr.reverse() - arr_map bool // []map[key]value - print_options bool // print option type - safe_int bool // needs safe int comparison - print_types map[int]bool // print() idx types - used_fns map[string]bool // filled in by markused - used_consts map[string]bool // filled in by markused - used_globals map[string]bool // filled in by markused - used_syms map[int]bool // filled in by markused - used_veb_types []Type // veb context types, filled in by checker - used_maps int // how many times maps were used, filled in by markused - used_none int // how many times `none` was used, filled in by markused - used_closures int // number of used closures, either directly with `fn [state] () {}`, or indirectly (though `instance.method` promotions) + dump bool // filled in by markused + anon_fn bool // fn () { } + auto_str bool // auto str fns + auto_str_ptr bool // auto str fns for ptr type + auto_str_arr bool // auto str fns for array + arr_prepend bool // arr.prepend() + arr_insert bool // arr.insert() + arr_first bool // arr.first() + arr_last bool // arr.last() + arr_pop_left bool // arr.pop_left() + arr_pop bool // arr.pop() + arr_delete bool // arr.delete() + arr_reverse bool // arr.reverse() + arr_map bool // []map[key]value + print_options bool // print option type + safe_int bool // needs safe int comparison + print_types map[int]bool // print() idx types + used_fns map[string]bool // filled in by markused + used_consts map[string]bool // filled in by markused + used_globals map[string]bool // filled in by markused + used_syms map[int]bool // filled in by markused + referenced_fns map[string]bool // filled in by the checker + referenced_consts map[string]bool // filled in by the checker + used_veb_types []Type // veb context types, filled in by checker + used_maps int // how many times maps were used, filled in by markused + used_none int // how many times `none` was used, filled in by markused + used_closures int // number of used closures, either directly with `fn [state] () {}`, or indirectly (though `instance.method` promotions) // json bool // json is imported comptime_calls map[string]bool // resolved name to call on comptime comptime_syms map[Type]bool // resolved syms (generic) @@ -52,6 +54,8 @@ pub fn (mut uf UsedFeatures) free() { uf.used_fns.free() uf.used_consts.free() uf.used_globals.free() + uf.referenced_fns.free() + uf.referenced_consts.free() uf.used_veb_types.free() } } diff --git a/vlib/v/checker/assign.v b/vlib/v/checker/assign.v index 6ff9cfb7d..25a716313 100644 --- a/vlib/v/checker/assign.v +++ b/vlib/v/checker/assign.v @@ -895,12 +895,14 @@ or use an explicit `unsafe{ a[..] }`, if you do not want a copy of the slice.', continue } if method := left_sym.find_method_with_generic_parent(extracted_op) { + c.mark_fn_decl_as_referenced(method.fkey()) if method.return_type != left_type_unwrapped { c.error('operator `${extracted_op}` must return `${left_name}` to be used as an assignment operator', node.pos) } } else { if method := parent_sym.find_method_with_generic_parent(extracted_op) { + c.mark_fn_decl_as_referenced(method.fkey()) if parent_sym.kind == .alias && (parent_sym.info as ast.Alias).parent_type != method.return_type { c.error('operator `${extracted_op}` must return `${left_name}` to be used as an assignment operator', diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index cf1184eef..252abb546 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -766,6 +766,8 @@ pub fn (mut c Checker) check_files(ast_files []&ast.File) { c.verify_all_vweb_routes() c.timers.show('checker_verify_all_vweb_routes') + c.check_unused_declarations(ast_files) + if c.pref.is_test { mut n_test_fns := 0 for _, f in c.table.fns { @@ -2493,6 +2495,15 @@ fn (mut c Checker) selector_expr(mut node ast.SelectorExpr) ast.Type { node.expr_type = typ typ = c.recheck_concrete_type(typ) node.expr_type = typ + const_name := c.module_qualified_selector_const_name(node) + if const_name != '' { + c.mark_const_decl_as_referenced(const_name) + if mut const_obj := c.table.global_scope.find_const(node.field_name) { + c.mark_const_decl_as_referenced(const_obj.name) + } + } else if mut const_obj := c.file.global_scope.find_const(node.str()) { + c.mark_const_decl_as_referenced(const_obj.name) + } if !(node.expr is ast.Ident && node.expr.kind == .constant) { if node.expr_type.has_flag(.option) { c.error('cannot access fields of an Option, handle the error with `or {...}` or propagate it with `?`', @@ -2671,6 +2682,7 @@ fn (mut c Checker) selector_expr(mut node ast.SelectorExpr) ast.Type { return node.typ } if mut method := c.table.sym(c.unwrap_generic(typ)).find_method_with_generic_parent(field_name) { + c.mark_fn_decl_as_referenced(method.fkey()) c.markused_comptime_call(typ.has_flag(.generic), '${int(method.params[0].typ)}.${field_name}') if c.expected_type != 0 && c.expected_type != ast.none_type { mut method_copy := method @@ -5472,6 +5484,7 @@ fn (mut c Checker) resolve_var_fn(func &ast.Fn, mut node ast.Ident, name string) node.info = ast.IdentFn{ typ: fn_type } + c.mark_fn_decl_as_referenced(func.fkey()) return fn_type } @@ -5514,6 +5527,9 @@ fn (mut c Checker) ident(mut node ast.Ident) ast.Type { } return ast.void_type } else if node.kind in [.constant, .global, .variable] { + if node.kind == .constant { + c.mark_const_decl_as_referenced(node.full_name()) + } // second use info := node.info as ast.IdentVar mut info_typ := info.typ @@ -5769,6 +5785,7 @@ fn (mut c Checker) ident(mut node ast.Ident) ast.Type { } obj.typ = typ node.obj = obj + c.mark_const_decl_as_referenced(obj.name) if obj.attrs.contains('deprecated') && obj.mod != c.mod { c.deprecate('const', obj.name, obj.attrs, node.pos) diff --git a/vlib/v/checker/comptime.v b/vlib/v/checker/comptime.v index 73f9a1a80..53010ff0e 100644 --- a/vlib/v/checker/comptime.v +++ b/vlib/v/checker/comptime.v @@ -280,6 +280,7 @@ fn (mut c Checker) comptime_call(mut node ast.ComptimeCall) ast.Type { c.error('could not find method `${method_name}`', node.method_pos) return ast.void_type } + c.mark_fn_decl_as_referenced(f.fkey()) c.markused_comptime_call(true, '${int(left_type)}.${method_name}') node.result_type = f.return_type return f.return_type diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index fd1ac6d55..9d3adb835 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -1446,6 +1446,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. found = true func = f unsafe { c.table.fns[name_prefixed].usages++ } + c.mark_fn_decl_as_referenced(f.fkey()) } } if !found && node.left is ast.IndexExpr { @@ -1502,6 +1503,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. found = true func = f unsafe { c.table.fns[fn_name].usages++ } + c.mark_fn_decl_as_referenced(f.fkey()) } } @@ -1517,6 +1519,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. func = f node.name = qualified_name unsafe { c.table.fns[qualified_name].usages++ } + c.mark_fn_decl_as_referenced(f.fkey()) if !c.table.register_fn_concrete_types(f.name, concrete_types) { c.need_recheck_generic_fns = true } @@ -1539,6 +1542,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. found = true func = f unsafe { c.table.fns[orig_name].usages++ } + c.mark_fn_decl_as_referenced(f.fkey()) node.name = orig_name node.left_type = typ } @@ -1605,6 +1609,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. unsafe { c.table.fns[fn_name].usages++ } found = true func = unsafe { c.table.fns[fn_name] } + c.mark_fn_decl_as_referenced(func.fkey()) is_native_builtin = true } } @@ -1623,6 +1628,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. found = true func = f unsafe { c.table.fns[os_name].usages++ } + c.mark_fn_decl_as_referenced(f.fkey()) } } if is_native_builtin { @@ -1742,6 +1748,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. node.is_fn_a_const = true node.fn_var_type = obj.typ node.const_name = qualified_const_name + c.mark_const_decl_as_referenced(qualified_const_name) } } } @@ -2991,6 +2998,7 @@ fn (mut c Checker) method_call(mut node ast.CallExpr, mut continue_check &bool) } return ast.void_type } + c.mark_fn_decl_as_referenced(method.fkey()) // x is Bar[T], x.foo() -> x.foo[T]() rec_sym := if is_method_from_embed { diff --git a/vlib/v/checker/infix.v b/vlib/v/checker/infix.v index 6cdd158d4..d4f4ee2b8 100644 --- a/vlib/v/checker/infix.v +++ b/vlib/v/checker/infix.v @@ -408,12 +408,14 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type { && !left_final_sym.is_primitive() { if left_sym.has_method(op_str) { if method := left_sym.find_method(op_str) { + c.mark_fn_decl_as_referenced(method.fkey()) return_type = method.return_type } else { return_type = left_type } } else if left_final_sym.has_method_with_generic_parent(op_str) { if method := left_final_sym.find_method_with_generic_parent(op_str) { + c.mark_fn_decl_as_referenced(method.fkey()) return_type = method.return_type } else { return_type = left_type @@ -433,24 +435,28 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type { && !right_final_sym.is_primitive() { if right_sym.has_method(op_str) { if method := right_sym.find_method(op_str) { + c.mark_fn_decl_as_referenced(method.fkey()) return_type = method.return_type } else { return_type = right_type } } else if right_final_sym.has_method_with_generic_parent(op_str) { if method := right_final_sym.find_method_with_generic_parent(op_str) { + c.mark_fn_decl_as_referenced(method.fkey()) return_type = method.return_type } else { return_type = right_type } } else if left_sym.has_method(op_str) { if method := left_sym.find_method(op_str) { + c.mark_fn_decl_as_referenced(method.fkey()) return_type = method.return_type } else { return_type = left_type } } else if left_final_sym.has_method_with_generic_parent(op_str) { if method := left_final_sym.find_method_with_generic_parent(op_str) { + c.mark_fn_decl_as_referenced(method.fkey()) return_type = method.return_type } else { return_type = left_type @@ -477,6 +483,7 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type { if !c.pref.translated && left_sym.kind in [.array, .array_fixed, .map, .struct] { if left_sym.has_method_with_generic_parent(op_str) { if method := left_sym.find_method_with_generic_parent(op_str) { + c.mark_fn_decl_as_referenced(method.fkey()) return_type = method.return_type } else { return_type = left_type @@ -495,6 +502,7 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type { } else if !c.pref.translated && right_sym.kind in [.array, .array_fixed, .map, .struct] { if right_sym.has_method_with_generic_parent(op_str) { if method := right_sym.find_method_with_generic_parent(op_str) { + c.mark_fn_decl_as_referenced(method.fkey()) return_type = method.return_type } else { return_type = right_type @@ -576,12 +584,14 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type { if left_sym.info is ast.Alias && left_final_sym.is_primitive() { if left_sym.has_method(op_str) { if method := left_sym.find_method(op_str) { + c.mark_fn_decl_as_referenced(method.fkey()) return_type = method.return_type } } } else if right_sym.info is ast.Alias && right_final_sym.is_primitive() { if right_sym.has_method(op_str) { if method := right_sym.find_method(op_str) { + c.mark_fn_decl_as_referenced(method.fkey()) return_type = method.return_type } } @@ -633,6 +643,8 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type { left_right_pos) } else if !left_sym.has_method('<') && node.op == .gt { c.error('cannot use `>` as `<=` operator method is not defined', left_right_pos) + } else if method := left_sym.find_method_with_generic_parent('<') { + c.mark_fn_decl_as_referenced(method.fkey()) } } else if left_type.has_flag(.generic) && right_type.has_flag(.generic) { // Try to unwrap the generic type to make sure that @@ -647,6 +659,8 @@ fn (mut c Checker) infix_expr(mut node ast.InfixExpr) ast.Type { } else if need_overload && !gen_sym.has_method_with_generic_parent('<') && node.op == .gt { c.error('cannot use `>` as `<=` operator method is not defined', left_right_pos) + } else if method := gen_sym.find_method_with_generic_parent('<') { + c.mark_fn_decl_as_referenced(method.fkey()) } } else if left_type in ast.integer_type_idxs && right_type in ast.integer_type_idxs { is_left_type_signed := left_type in ast.signed_integer_type_idxs diff --git a/vlib/v/checker/tests/auto_ref_voidptr.out b/vlib/v/checker/tests/auto_ref_voidptr.out index 546ea1c68..9de83c91f 100644 --- a/vlib/v/checker/tests/auto_ref_voidptr.out +++ b/vlib/v/checker/tests/auto_ref_voidptr.out @@ -22,3 +22,10 @@ vlib/v/checker/tests/auto_ref_voidptr.vv:9:9: warning: automatic Aa referencing/ | ^ 10 | // foo2(a) 11 | } +vlib/v/checker/tests/auto_ref_voidptr.vv:3:4: warning: unused function: `foo2` + 1 | fn foo(x int, p voidptr) {} + 2 | + 3 | fn foo2(p &Aa) {} + | ~~~~ + 4 | + 5 | struct Aa {} diff --git a/vlib/v/checker/tests/globals_run/function_stored_in_global.run.out b/vlib/v/checker/tests/globals_run/function_stored_in_global.run.out index a38dd88cc..9ab274619 100644 --- a/vlib/v/checker/tests/globals_run/function_stored_in_global.run.out +++ b/vlib/v/checker/tests/globals_run/function_stored_in_global.run.out @@ -1,3 +1,10 @@ +vlib/v/checker/tests/globals_run/function_stored_in_global.vv:4:4: warning: unused function: `current` + 2 | cpu_get_id fn () u64 + 3 | ) + 4 | fn current() u64 { + | ~~~~~~~ + 5 | return cpu_get_id() + 6 | } [vlib/v/checker/tests/globals_run/function_stored_in_global.vv:14] voidptr(abc) == voidptr(cpu_get_id): true [vlib/v/checker/tests/globals_run/function_stored_in_global.vv:15] cpu_get_id(): 123 [vlib/v/checker/tests/globals_run/function_stored_in_global.vv:16] abc(): 123 diff --git a/vlib/v/checker/tests/modules/unused_private_declarations.out b/vlib/v/checker/tests/modules/unused_private_declarations.out new file mode 100644 index 000000000..62735a83b --- /dev/null +++ b/vlib/v/checker/tests/modules/unused_private_declarations.out @@ -0,0 +1,14 @@ +vlib/v/checker/tests/modules/unused_private_declarations/main.v:4:7: error: unused constant: `unused_const` + 2 | const used_by_pub_const = 3 + 3 | const const_used_by_unused_fn = 2 + 4 | const unused_const = 4 + | ~~~~~~~~~~~~~~~~ + 5 | + 6 | fn used_fn() int { +vlib/v/checker/tests/modules/unused_private_declarations/main.v:14:4: error: unused function: `unused_fn` + 12 | } + 13 | + 14 | fn unused_fn() int { + | ~~~~~~~~~ + 15 | return const_used_by_unused_fn + 16 | } diff --git a/vlib/v/checker/tests/modules/unused_private_declarations/main.v b/vlib/v/checker/tests/modules/unused_private_declarations/main.v new file mode 100644 index 000000000..409ee44a1 --- /dev/null +++ b/vlib/v/checker/tests/modules/unused_private_declarations/main.v @@ -0,0 +1,32 @@ +const used_const = 1 +const used_by_pub_const = 3 +const const_used_by_unused_fn = 2 +const unused_const = 4 + +fn used_fn() int { + return used_const +} + +fn helper_used_by_pub() int { + return used_by_pub_const +} + +fn unused_fn() int { + return const_used_by_unused_fn +} + +pub fn pub_fn() int { + return helper_used_by_pub() +} + +$if never_defined ? { + const skipped_const = 4 + + fn skipped_fn() int { + return skipped_const + } +} + +fn main() { + println(used_fn()) +} diff --git a/vlib/v/checker/tests/return_working_comp_if.out b/vlib/v/checker/tests/return_working_comp_if.out index e69de29bb..99085f521 100644 --- a/vlib/v/checker/tests/return_working_comp_if.out +++ b/vlib/v/checker/tests/return_working_comp_if.out @@ -0,0 +1,7 @@ +vlib/v/checker/tests/return_working_comp_if.vv:3:4: warning: unused function: `foo` + 1 | fn main() {} + 2 | + 3 | fn foo() string { + | ~~~ + 4 | $if windows { + 5 | return '' diff --git a/vlib/v/checker/tests/return_working_comp_if_nested.out b/vlib/v/checker/tests/return_working_comp_if_nested.out index e69de29bb..442f64705 100644 --- a/vlib/v/checker/tests/return_working_comp_if_nested.out +++ b/vlib/v/checker/tests/return_working_comp_if_nested.out @@ -0,0 +1,7 @@ +vlib/v/checker/tests/return_working_comp_if_nested.vv:3:4: warning: unused function: `foo` + 1 | fn main() {} + 2 | + 3 | fn foo() string { + | ~~~ + 4 | a := 1 + 5 | b := 1 diff --git a/vlib/v/checker/tests/return_working_if_match.out b/vlib/v/checker/tests/return_working_if_match.out index f7e84c03a..5e9ef1cc0 100644 --- a/vlib/v/checker/tests/return_working_if_match.out +++ b/vlib/v/checker/tests/return_working_if_match.out @@ -19,3 +19,10 @@ vlib/v/checker/tests/return_working_if_match.vv:7:4: notice: match is always fal | ^ 8 | else { return '' } 9 | } +vlib/v/checker/tests/return_working_if_match.vv:3:4: warning: unused function: `foo` + 1 | fn main() {} + 2 | + 3 | fn foo() string { + | ~~~ + 4 | if true { + 5 | match 1 { diff --git a/vlib/v/checker/tests/return_working_match_if.out b/vlib/v/checker/tests/return_working_match_if.out index 0c157486c..ddff899ea 100644 --- a/vlib/v/checker/tests/return_working_match_if.out +++ b/vlib/v/checker/tests/return_working_match_if.out @@ -19,3 +19,10 @@ vlib/v/checker/tests/return_working_match_if.vv:9:7: notice: condition is always | ~~~~ 10 | return '' 11 | } else { +vlib/v/checker/tests/return_working_match_if.vv:3:4: warning: unused function: `foo` + 1 | fn main() {} + 2 | + 3 | fn foo() string { + | ~~~ + 4 | match 1 { + 5 | 1 { diff --git a/vlib/v/checker/tests/return_working_nested.out b/vlib/v/checker/tests/return_working_nested.out index 667144227..93e4ad9d8 100644 --- a/vlib/v/checker/tests/return_working_nested.out +++ b/vlib/v/checker/tests/return_working_nested.out @@ -12,3 +12,10 @@ vlib/v/checker/tests/return_working_nested.vv:7:6: notice: condition is always t | ~~~~ 8 | return '' 9 | } +vlib/v/checker/tests/return_working_nested.vv:3:4: warning: unused function: `foo` + 1 | fn main() {} + 2 | + 3 | fn foo() string { + | ~~~ + 4 | if true { + 5 | return '' diff --git a/vlib/v/checker/tests/return_working_simple.out b/vlib/v/checker/tests/return_working_simple.out index 6000815f2..785b9fc16 100644 --- a/vlib/v/checker/tests/return_working_simple.out +++ b/vlib/v/checker/tests/return_working_simple.out @@ -5,3 +5,10 @@ vlib/v/checker/tests/return_working_simple.vv:4:5: notice: condition is always t | ~~~~ 5 | return '' 6 | } else { +vlib/v/checker/tests/return_working_simple.vv:3:4: warning: unused function: `foo` + 1 | fn main() {} + 2 | + 3 | fn foo() string { + | ~~~ + 4 | if true { + 5 | return '' diff --git a/vlib/v/checker/tests/return_working_two_if.out b/vlib/v/checker/tests/return_working_two_if.out index 49ca3dd4d..fe159a8f0 100644 --- a/vlib/v/checker/tests/return_working_two_if.out +++ b/vlib/v/checker/tests/return_working_two_if.out @@ -19,3 +19,10 @@ vlib/v/checker/tests/return_working_two_if.vv:9:6: notice: condition is always t | ~~~~ 10 | return '' 11 | } else { +vlib/v/checker/tests/return_working_two_if.vv:3:4: warning: unused function: `foo` + 1 | fn main() {} + 2 | + 3 | fn foo() string { + | ~~~ + 4 | if true { + 5 | if true { diff --git a/vlib/v/checker/tests/return_working_unsafe.out b/vlib/v/checker/tests/return_working_unsafe.out index e69de29bb..401ed6134 100644 --- a/vlib/v/checker/tests/return_working_unsafe.out +++ b/vlib/v/checker/tests/return_working_unsafe.out @@ -0,0 +1,7 @@ +vlib/v/checker/tests/return_working_unsafe.vv:3:4: warning: unused function: `foo` + 1 | fn main() {} + 2 | + 3 | fn foo() string { + | ~~~ + 4 | unsafe { + 5 | return '' diff --git a/vlib/v/checker/tests/unused_private_declarations.out b/vlib/v/checker/tests/unused_private_declarations.out new file mode 100644 index 000000000..708395d8d --- /dev/null +++ b/vlib/v/checker/tests/unused_private_declarations.out @@ -0,0 +1,14 @@ +vlib/v/checker/tests/unused_private_declarations.vv:4:7: warning: unused constant: `unused_const` + 2 | const used_by_pub_const = 3 + 3 | const const_used_by_unused_fn = 2 + 4 | const unused_const = 4 + | ~~~~~~~~~~~~~~~~ + 5 | + 6 | fn used_fn() int { +vlib/v/checker/tests/unused_private_declarations.vv:14:4: warning: unused function: `unused_fn` + 12 | } + 13 | + 14 | fn unused_fn() int { + | ~~~~~~~~~ + 15 | return const_used_by_unused_fn + 16 | } diff --git a/vlib/v/checker/tests/unused_private_declarations.vv b/vlib/v/checker/tests/unused_private_declarations.vv new file mode 100644 index 000000000..409ee44a1 --- /dev/null +++ b/vlib/v/checker/tests/unused_private_declarations.vv @@ -0,0 +1,32 @@ +const used_const = 1 +const used_by_pub_const = 3 +const const_used_by_unused_fn = 2 +const unused_const = 4 + +fn used_fn() int { + return used_const +} + +fn helper_used_by_pub() int { + return used_by_pub_const +} + +fn unused_fn() int { + return const_used_by_unused_fn +} + +pub fn pub_fn() int { + return helper_used_by_pub() +} + +$if never_defined ? { + const skipped_const = 4 + + fn skipped_fn() int { + return skipped_const + } +} + +fn main() { + println(used_fn()) +} diff --git a/vlib/v/checker/tests/use_deprecated_function_warning.out b/vlib/v/checker/tests/use_deprecated_function_warning.out index b601b4d0a..35bbc3ac9 100644 --- a/vlib/v/checker/tests/use_deprecated_function_warning.out +++ b/vlib/v/checker/tests/use_deprecated_function_warning.out @@ -18,3 +18,10 @@ vlib/v/checker/tests/use_deprecated_function_warning.vv:23:4: warning: method `S 23 | s.m() | ~~~ 24 | } +vlib/v/checker/tests/use_deprecated_function_warning.vv:21:4: warning: unused function: `method` + 19 | fn (s S1) m() {} + 20 | + 21 | fn method() { + | ~~~~~~ + 22 | s := S1{} + 23 | s.m() diff --git a/vlib/v/checker/tests/var_duplicate_const.out b/vlib/v/checker/tests/var_duplicate_const.out index fa2301354..676887eb1 100644 --- a/vlib/v/checker/tests/var_duplicate_const.out +++ b/vlib/v/checker/tests/var_duplicate_const.out @@ -12,3 +12,8 @@ vlib/v/checker/tests/var_duplicate_const.vv:4:5: warning: unused variable: `size | ~~~~ 5 | println(main.size) 6 | } +vlib/v/checker/tests/var_duplicate_const.vv:1:7: warning: unused constant: `size` + 1 | const size = 22 + | ~~~~~~~~~ + 2 | + 3 | fn main() { diff --git a/vlib/v/checker/unused_declarations.v b/vlib/v/checker/unused_declarations.v new file mode 100644 index 000000000..d419bb257 --- /dev/null +++ b/vlib/v/checker/unused_declarations.v @@ -0,0 +1,139 @@ +module checker + +import v.ast + +@[inline] +fn (mut c Checker) mark_fn_decl_as_referenced(fkey string) { + if fkey == '' { + return + } + c.table.used_features.referenced_fns[fkey] = true +} + +@[inline] +fn (mut c Checker) mark_const_decl_as_referenced(name string) { + if name == '' { + return + } + c.table.used_features.referenced_consts[name] = true +} + +fn (mut c Checker) mark_type_str_method_as_referenced(typ ast.Type) { + sym := c.table.sym(c.unwrap_generic(typ)) + if method := sym.find_method_with_generic_parent('str') { + c.mark_fn_decl_as_referenced(method.fkey()) + } +} + +fn (c &Checker) module_qualified_selector_const_name(node ast.SelectorExpr) string { + if root_ident := node.root_ident() { + if root_ident.name == c.mod { + return '${c.mod}.${node.field_name}' + } + for import_sym in c.file.imports { + alias := if import_sym.alias == '' { + import_sym.mod.all_after_last('.') + } else { + import_sym.alias + } + if root_ident.name == alias { + return '${import_sym.mod}.${node.field_name}' + } + } + } + return '' +} + +fn (mut c Checker) check_unused_declarations(ast_files []&ast.File) { + if c.pref.is_repl || c.pref.is_test || c.pref.check_only || c.pref.only_check_syntax + || c.pref.translated || c.nr_errors > 0 || c.main_fn_decl_node.name == '' { + return + } + saved_file := c.file + for file in ast_files { + if file.mod.name != 'main' || file.is_test || file.is_generated || file.is_translated { + continue + } + c.change_current_file(file) + c.check_unused_declarations_in_stmts(file.stmts) + if c.should_abort { + break + } + } + if saved_file != unsafe { nil } { + c.change_current_file(saved_file) + } +} + +fn (mut c Checker) check_unused_declarations_in_stmts(stmts []ast.Stmt) { + for stmt in stmts { + match stmt { + ast.ConstDecl { + for field in stmt.fields { + if c.should_warn_about_unused_const(field) { + c.warn('unused constant: `${stripped_name(field.name)}`', field.pos) + } + } + } + ast.FnDecl { + if c.should_warn_about_unused_fn(stmt) { + name := if stmt.short_name != '' { stmt.short_name } else { stmt.get_name() } + c.warn('unused function: `${name}`', stmt.name_pos) + } + } + ast.ExprStmt { + match stmt.expr { + ast.IfExpr { + if stmt.expr.is_comptime { + for branch in stmt.expr.branches { + if c.is_active_comptime_branch(branch.id) { + c.check_unused_declarations_in_stmts(branch.stmts) + } + } + } + } + ast.MatchExpr { + if stmt.expr.is_comptime { + for branch in stmt.expr.branches { + if c.is_active_comptime_branch(branch.id) { + c.check_unused_declarations_in_stmts(branch.stmts) + } + } + } + } + else {} + } + } + else {} + } + if c.should_abort { + return + } + } +} + +fn (c &Checker) is_active_comptime_branch(branch_id int) bool { + if branch_id == 0 { + return true + } + if result := c.table.comptime_is_true['|id=${branch_id}|'] { + return result.val + } + return true +} + +fn (c &Checker) should_warn_about_unused_const(field ast.ConstField) bool { + if field.is_pub || field.is_markused || field.is_exported || field.is_virtual_c { + return false + } + return field.name !in c.table.used_features.referenced_consts +} + +fn (c &Checker) should_warn_about_unused_fn(node ast.FnDecl) bool { + if node.is_pub || node.is_markused || node.is_exported || node.no_body || node.is_main + || node.is_test || node.is_method || node.is_static_type_method || node.should_be_skipped + || node.short_name in ['init', 'cleanup'] { + return false + } + return node.fkey() !in c.table.used_features.referenced_fns +} diff --git a/vlib/v/checker/used_features.v b/vlib/v/checker/used_features.v index 5da3e36e9..601498da2 100644 --- a/vlib/v/checker/used_features.v +++ b/vlib/v/checker/used_features.v @@ -124,6 +124,7 @@ fn (mut c Checker) markused_print_call(mut node ast.CallExpr) { || !c.table.sym(arg_typ).has_method('str') { c.table.used_features.auto_str = true } else { + c.mark_type_str_method_as_referenced(arg_typ) if arg_typ.has_option_or_result() { c.table.used_features.print_options = true } @@ -184,6 +185,7 @@ fn (mut c Checker) markused_string_inter_lit(mut _ ast.StringInterLiteral, ftyp if !c.table.sym(ftyp).has_method('str') { c.table.used_features.auto_str = true } else { + c.mark_type_str_method_as_referenced(ftyp) c.table.used_features.print_types[ftyp.idx()] = true } if ftyp.is_ptr() { -- 2.39.5