Skip to main content

gobject_linter/rules/
dead_code.rs

1use std::collections::{HashMap, HashSet};
2
3use gobject_ast::model::{
4    AssignmentOp, Designator, Expression, FileModel, GObjectTypeKind, Parameter,
5    PreprocessorDirective, SourceLocation, Statement, StructField, TopLevelItem, TypeDefItem,
6    TypeInfo, VariableDecl,
7};
8
9use crate::{
10    ast_context::AstContext,
11    config::Config,
12    rules::{Category, Rule, Violation},
13};
14
15pub struct DeadCode;
16
17impl Rule for DeadCode {
18    fn name(&self) -> &'static str {
19        "dead_code"
20    }
21
22    fn description(&self) -> &'static str {
23        "Detect unused internal functions and types"
24    }
25
26    fn long_description(&self) -> Option<&'static str> {
27        Some(
28            "Detects internal functions and types that are never used anywhere in the codebase. \
29             For functions: tracks both direct calls and function pointer usage (e.g., callbacks). \
30             For types: tracks usage in variable declarations, casts, sizeof, and GObject macros. \
31             Only reports items in private headers (not installed by meson) and static functions/types \
32             defined in .c files.",
33        )
34    }
35
36    fn category(&self) -> Category {
37        Category::Suspicious
38    }
39
40    fn requires_meson(&self) -> bool {
41        true
42    }
43
44    fn opt_in(&self) -> bool {
45        true
46    }
47
48    fn opt_in_reason(&self) -> Option<&'static str> {
49        Some(
50            "May produce false positives due to fundamental limitations of static analysis without a preprocessor or full call graph",
51        )
52    }
53
54    fn check_all(
55        &self,
56        ast_context: &AstContext,
57        _config: &Config,
58        violations: &mut Vec<Violation>,
59    ) {
60        if !ast_context.has_public_private_info() {
61            return;
62        }
63
64        let (func_defs, func_decls) = collect_function_maps(ast_context);
65        let refs = collect_all_refs(ast_context);
66
67        let (type_defs, field_defs, enum_value_defs) = collect_private_defs(ast_context);
68
69        self.report_function_violations(
70            ast_context,
71            &func_defs,
72            &func_decls,
73            &refs.func,
74            violations,
75        );
76        self.report_type_violations(ast_context, &type_defs, &refs.types, violations);
77        self.report_enum_value_violations(&enum_value_defs, &refs.func, violations);
78        self.report_field_violations(
79            ast_context,
80            &field_defs,
81            &refs.field_qualified,
82            &refs.field_unqualified,
83            violations,
84        );
85    }
86}
87
88fn collect_all_refs(ast_context: &AstContext) -> AllRefs {
89    let mut func_refs: HashSet<String> = HashSet::new();
90    let mut type_refs: HashSet<String> = HashSet::new();
91    let mut field_qualified: HashMap<String, HashSet<String>> = HashMap::new();
92    let mut field_unqualified: HashSet<String> = HashSet::new();
93    let empty_map: HashMap<&str, String> = HashMap::new();
94
95    for (_path, file) in ast_context.iter_all_files() {
96        for func in file.iter_function_definitions() {
97            collect_type_ref(&func.return_type, &mut type_refs);
98            for param in &func.parameters {
99                if let Parameter::Regular { type_info, .. } = param {
100                    collect_type_ref(type_info, &mut type_refs);
101                }
102            }
103
104            let type_map: HashMap<&str, String> = func
105                .local_var_types()
106                .into_iter()
107                .filter(|(_, ti)| !ti.base_type.is_empty())
108                .map(|(name, ti)| {
109                    (
110                        name,
111                        ast_context
112                            .type_aliases()
113                            .canonical(&ti.base_type)
114                            .to_owned(),
115                    )
116                })
117                .collect();
118
119            for stmt in &func.body_statements {
120                collect_func_refs_from_stmt(stmt, &mut func_refs);
121                collect_type_refs_from_stmt(stmt, &mut type_refs);
122                collect_field_refs_from_stmt(
123                    ast_context,
124                    stmt,
125                    &type_map,
126                    &mut field_qualified,
127                    &mut field_unqualified,
128                );
129            }
130        }
131
132        for func in file.iter_function_declarations() {
133            collect_type_ref(&func.return_type, &mut type_refs);
134            for param in &func.parameters {
135                if let Parameter::Regular { type_info, .. } = param {
136                    collect_type_ref(type_info, &mut type_refs);
137                }
138            }
139        }
140
141        for item in file.iter_all_items() {
142            if let TopLevelItem::Declaration(decl) = item {
143                collect_func_refs_from_decl(decl, &mut func_refs);
144                collect_field_refs_from_decl(
145                    ast_context,
146                    decl,
147                    &empty_map,
148                    &mut field_qualified,
149                    &mut field_unqualified,
150                );
151            }
152            collect_type_refs_from_top_level_item(item, &mut type_refs);
153            match item {
154                TopLevelItem::Preprocessor(PreprocessorDirective::Define {
155                    value: Some(value),
156                    ..
157                }) => {
158                    extract_function_calls_from_text(value.as_raw_str(), &mut func_refs);
159                    extract_field_refs_from_text(value.as_raw_str(), &mut field_unqualified);
160                }
161                TopLevelItem::Preprocessor(
162                    PreprocessorDirective::AutoptrCleanupFunc {
163                        type_name,
164                        cleanup_function,
165                        ..
166                    }
167                    | PreprocessorDirective::AutoCleanupClearFunc {
168                        type_name,
169                        cleanup_function,
170                        ..
171                    },
172                ) => {
173                    func_refs.insert(cleanup_function.clone());
174                    type_refs.insert(type_name.clone());
175                }
176                _ => {}
177            }
178        }
179
180        collect_gobject_implicit_refs(file, &mut func_refs, &mut type_refs);
181
182        for enum_info in file.iter_all_enums() {
183            for value in &enum_info.values {
184                if let Some(expr) = &value.value_expr {
185                    func_refs.extend(expr.collect_identifiers());
186                }
187            }
188        }
189    }
190
191    AllRefs {
192        func: func_refs,
193        types: type_refs,
194        field_qualified,
195        field_unqualified,
196    }
197}
198
199type FuncDefMap<'a> = HashMap<&'a str, Vec<(&'a std::path::Path, bool, &'a SourceLocation)>>;
200type FuncDeclMap<'a> = HashMap<&'a str, Vec<(&'a std::path::Path, &'a SourceLocation)>>;
201
202fn collect_function_maps<'a>(ast_context: &'a AstContext) -> (FuncDefMap<'a>, FuncDeclMap<'a>) {
203    let mut defs: FuncDefMap = HashMap::new();
204    let mut decls: FuncDeclMap = HashMap::new();
205
206    for (path, file) in ast_context.iter_all_files() {
207        let ext = path.extension().and_then(|e| e.to_str());
208        if ext == Some("c") {
209            for func in file.iter_function_definitions() {
210                defs.entry(func.name.as_str()).or_default().push((
211                    path,
212                    func.is_static,
213                    &func.location,
214                ));
215            }
216        }
217        if ext == Some("h") {
218            for func in file.iter_function_declarations() {
219                decls
220                    .entry(func.name.as_str())
221                    .or_default()
222                    .push((path, &func.location));
223            }
224        }
225    }
226
227    (defs, decls)
228}
229
230struct AllRefs {
231    func: HashSet<String>,
232    types: HashSet<String>,
233    field_qualified: HashMap<String, HashSet<String>>,
234    field_unqualified: HashSet<String>,
235}
236
237type TypeDefMap<'a> = HashMap<&'a str, Vec<(&'a std::path::Path, &'a SourceLocation)>>;
238
239type EnumValueDefMap<'a> = HashMap<&'a str, Vec<(&'a std::path::Path, &'a SourceLocation)>>;
240
241type FieldDefMap<'a> = HashMap<&'a str, Vec<(&'a std::path::Path, &'a SourceLocation, &'a str)>>;
242
243fn collect_private_defs<'a>(
244    ast_context: &'a AstContext,
245) -> (TypeDefMap<'a>, FieldDefMap<'a>, EnumValueDefMap<'a>) {
246    let mut type_defs: TypeDefMap = HashMap::new();
247    let mut field_defs: FieldDefMap = HashMap::new();
248    let mut enum_value_defs: EnumValueDefMap = HashMap::new();
249
250    for (path, file) in ast_context.iter_private_files() {
251        for item in file.iter_all_items() {
252            match item {
253                TopLevelItem::TypeDefinition(TypeDefItem::Struct { name, location, .. }) => {
254                    type_defs
255                        .entry(name.as_str())
256                        .or_default()
257                        .push((path, location));
258                }
259                TopLevelItem::TypeDefinition(TypeDefItem::Typedef { name, location, .. }) => {
260                    type_defs
261                        .entry(name.as_str())
262                        .or_default()
263                        .push((path, location));
264                }
265                _ => {}
266            }
267
268            match item {
269                TopLevelItem::TypeDefinition(td @ TypeDefItem::Struct { name, fields, .. })
270                    if !td.is_vtable_struct() =>
271                {
272                    collect_fields_into_defs(fields, name, path, &mut field_defs);
273                }
274                TopLevelItem::TypeDefinition(
275                    td @ TypeDefItem::Typedef {
276                        name,
277                        struct_fields,
278                        ..
279                    },
280                ) if !td.is_vtable_struct() => {
281                    collect_fields_into_defs(struct_fields, name, path, &mut field_defs);
282                }
283                _ => {}
284            }
285        }
286
287        for enum_info in file.iter_all_enums() {
288            for value in &enum_info.values {
289                if value.is_prop_0()
290                    || value.is_prop_last()
291                    || value.is_signal_last()
292                    || (value.value == Some(0) && value.value_expr.is_some())
293                {
294                    continue;
295                }
296                enum_value_defs
297                    .entry(value.name.as_str())
298                    .or_default()
299                    .push((path, &value.location));
300            }
301        }
302    }
303
304    (type_defs, field_defs, enum_value_defs)
305}
306
307fn collect_fields_into_defs<'a>(
308    fields: &'a [StructField],
309    struct_name: &'a str,
310    path: &'a std::path::Path,
311    defs: &mut FieldDefMap<'a>,
312) {
313    let last_non_reserved = fields.iter().rposition(|f| !f.is_reserved());
314
315    for (idx, field) in fields.iter().enumerate() {
316        let Some(field_name) = &field.field_name else {
317            continue;
318        };
319        if !field.inner_fields.is_empty() {
320            collect_fields_into_defs(&field.inner_fields, struct_name, path, defs);
321            continue;
322        }
323        if idx == 0 && field_name.starts_with("parent") {
324            continue;
325        }
326        if field.is_reserved() && last_non_reserved.is_none_or(|last| idx > last) {
327            continue;
328        }
329        defs.entry(field_name.as_str())
330            .or_default()
331            .push((path, &field.location, struct_name));
332    }
333}
334
335fn collect_gobject_implicit_refs(
336    file: &FileModel,
337    func_refs: &mut HashSet<String>,
338    type_refs: &mut HashSet<String>,
339) {
340    for gt in file.iter_all_gobject_types() {
341        if gt.is_interface() {
342            func_refs.insert(gt.default_init_function_name());
343        } else if !matches!(
344            gt.kind,
345            GObjectTypeKind::DefineEnum { .. }
346                | GObjectTypeKind::DefineFlags { .. }
347                | GObjectTypeKind::DefineQuark { .. }
348                | GObjectTypeKind::DefineCustom { .. }
349        ) {
350            func_refs.insert(gt.class_init_function_name());
351            func_refs.insert(gt.init_function_name());
352        }
353
354        for iface in &gt.interfaces {
355            if let Some(init_func) = &iface.init_function {
356                func_refs.insert(init_func.clone());
357            }
358        }
359
360        if let GObjectTypeKind::DefineBoxed {
361            copy_func,
362            free_func,
363        } = &gt.kind
364        {
365            func_refs.insert(copy_func.clone());
366            func_refs.insert(free_func.clone());
367        }
368
369        if let Some(quark_fn) = gt.kind.quark_function_name() {
370            func_refs.insert(quark_fn);
371        }
372
373        if gt.has_private {
374            let priv_name = format!("{}Private", gt.type_name);
375            type_refs.insert(format!("_{priv_name}"));
376            type_refs.insert(priv_name);
377        }
378
379        let tn = &gt.type_name;
380        type_refs.insert(format!("_{tn}"));
381        if gt.is_interface() {
382            type_refs.insert(format!("_{tn}Interface"));
383        } else if !matches!(
384            gt.kind,
385            GObjectTypeKind::DefineBoxed { .. }
386                | GObjectTypeKind::DefineEnum { .. }
387                | GObjectTypeKind::DefineFlags { .. }
388                | GObjectTypeKind::DefineQuark { .. }
389                | GObjectTypeKind::DefineCustom { .. }
390        ) {
391            type_refs.insert(format!("_{tn}Class"));
392        }
393
394        for stmt in &gt.code_block_statements {
395            collect_func_refs_from_stmt(stmt, func_refs);
396            collect_type_refs_from_stmt(stmt, type_refs);
397        }
398    }
399}
400
401fn collect_type_ref(type_info: &TypeInfo, refs: &mut HashSet<String>) {
402    if !type_info.base_type.is_empty() {
403        refs.insert(type_info.base_type.clone());
404    }
405}
406
407fn collect_type_refs_from_stmt(stmt: &Statement, refs: &mut HashSet<String>) {
408    stmt.walk(&mut |s| {
409        if let Statement::Declaration(decl) = s {
410            collect_type_ref(&decl.type_info, refs);
411        }
412    });
413    stmt.walk_expressions(&mut |expr| {
414        expr.walk(&mut |e| match e {
415            Expression::Cast(cast) => collect_type_ref(&cast.type_info, refs),
416            Expression::Sizeof(sizeof) => {
417                if let Some(name) = sizeof.type_name()
418                    && !name.is_empty()
419                {
420                    refs.insert(name.to_owned());
421                }
422            }
423            _ => {}
424        });
425    });
426}
427
428fn collect_type_refs_from_top_level_item(item: &TopLevelItem, refs: &mut HashSet<String>) {
429    match item {
430        TopLevelItem::Declaration(decl) => collect_type_ref(&decl.type_info, refs),
431        TopLevelItem::Expression(_) => {}
432        TopLevelItem::TypeDefinition(TypeDefItem::Typedef {
433            target,
434            struct_fields,
435            ..
436        }) => {
437            if let Some(target_type) = target.as_type() {
438                collect_type_ref(target_type, refs);
439            }
440            collect_type_refs_from_fields(struct_fields, refs);
441        }
442        TopLevelItem::TypeDefinition(TypeDefItem::Struct { fields, .. }) => {
443            collect_type_refs_from_fields(fields, refs);
444        }
445        _ => {}
446    }
447}
448
449fn collect_type_refs_from_fields(fields: &[StructField], refs: &mut HashSet<String>) {
450    for field in fields {
451        field.walk(&mut |f| collect_type_ref(&f.field_type, refs));
452    }
453}
454
455fn collect_func_refs_from_stmt(stmt: &Statement, refs: &mut HashSet<String>) {
456    stmt.walk_expressions(&mut |expr| refs.extend(expr.collect_identifiers()));
457    stmt.walk(&mut |s| {
458        if let Statement::Preprocessor(PreprocessorDirective::Define {
459            value: Some(value), ..
460        }) = s
461        {
462            extract_function_calls_from_text(value.as_raw_str(), refs);
463        }
464    });
465}
466
467fn collect_func_refs_from_decl(decl: &VariableDecl, refs: &mut HashSet<String>) {
468    if let Some(init) = &decl.initializer {
469        init.walk(&mut |e| refs.extend(e.collect_identifiers()));
470    }
471    if let Some(size) = &decl.array_size {
472        size.walk(&mut |e| refs.extend(e.collect_identifiers()));
473    }
474}
475
476fn collect_field_refs_from_decl(
477    ast_context: &AstContext,
478    decl: &VariableDecl,
479    type_map: &HashMap<&str, String>,
480    qualified: &mut HashMap<String, HashSet<String>>,
481    unqualified: &mut HashSet<String>,
482) {
483    if let Some(init) = &decl.initializer {
484        collect_field_reads_impl(ast_context, init, false, type_map, qualified, unqualified);
485    }
486    if let Some(size) = &decl.array_size {
487        collect_field_reads_impl(ast_context, size, false, type_map, qualified, unqualified);
488    }
489}
490
491fn extract_function_calls_from_text(text: &str, refs: &mut HashSet<String>) {
492    let mut chars = text.chars().peekable();
493    let mut ident = String::new();
494
495    while let Some(c) = chars.next() {
496        if c.is_alphanumeric() || c == '_' {
497            ident.push(c);
498        } else {
499            if !ident.is_empty() {
500                if c == '(' {
501                    refs.insert(ident.clone());
502                } else if c.is_whitespace() || c == '\\' {
503                    let mut lookahead = chars.clone();
504                    while let Some(&nc) = lookahead.peek() {
505                        if nc.is_whitespace() || nc == '\\' {
506                            lookahead.next();
507                        } else {
508                            if nc == '(' {
509                                refs.insert(ident.clone());
510                            }
511                            break;
512                        }
513                    }
514                }
515                ident.clear();
516            }
517        }
518    }
519}
520
521fn collect_field_refs_from_stmt(
522    ast_context: &AstContext,
523    stmt: &Statement,
524    type_map: &HashMap<&str, String>,
525    qualified: &mut HashMap<String, HashSet<String>>,
526    unqualified: &mut HashSet<String>,
527) {
528    stmt.walk_expressions(&mut |expr| {
529        collect_field_reads_impl(ast_context, expr, false, type_map, qualified, unqualified);
530    });
531
532    stmt.walk(&mut |s| {
533        if let Statement::Preprocessor(PreprocessorDirective::Define {
534            value: Some(value), ..
535        }) = s
536        {
537            extract_field_refs_from_text(value.as_raw_str(), unqualified);
538        }
539    });
540}
541
542/// `is_write_lhs` is true when `expr` is the direct LHS of a plain `=`
543/// assignment
544fn collect_field_reads_impl(
545    ast_context: &AstContext,
546    expr: &Expression,
547    is_write_lhs: bool,
548    type_map: &HashMap<&str, String>,
549    qualified: &mut HashMap<String, HashSet<String>>,
550    unqualified: &mut HashSet<String>,
551) {
552    match expr {
553        Expression::FieldAccess(f) => {
554            if !is_write_lhs {
555                if let Expression::Identifier(id) = f.base.as_ref()
556                    && let Some(type_name) = type_map.get(id.name.as_str())
557                {
558                    ast_context
559                        .type_aliases()
560                        .insert_qualified(type_name, &f.field, qualified);
561                } else {
562                    unqualified.insert(f.field.clone());
563                }
564            }
565            collect_field_reads_impl(
566                ast_context,
567                &f.base,
568                false,
569                type_map,
570                qualified,
571                unqualified,
572            );
573        }
574        Expression::Assignment(assign) => {
575            let lhs_is_write = assign.operator == AssignmentOp::Assign;
576            collect_field_reads_impl(
577                ast_context,
578                &assign.lhs,
579                lhs_is_write,
580                type_map,
581                qualified,
582                unqualified,
583            );
584            collect_field_reads_impl(
585                ast_context,
586                &assign.rhs,
587                false,
588                type_map,
589                qualified,
590                unqualified,
591            );
592        }
593        Expression::InitializerList(init) => {
594            for item in &init.items {
595                if let Some(Designator::Field(name)) = &item.designator {
596                    unqualified.insert(name.clone());
597                }
598                if let Some(Designator::Subscript(idx)) = &item.designator {
599                    collect_field_reads_impl(
600                        ast_context,
601                        idx,
602                        false,
603                        type_map,
604                        qualified,
605                        unqualified,
606                    );
607                }
608                collect_field_reads_impl(
609                    ast_context,
610                    &item.value,
611                    false,
612                    type_map,
613                    qualified,
614                    unqualified,
615                );
616            }
617        }
618        Expression::Call(call) => {
619            collect_field_reads_impl(
620                ast_context,
621                &call.function,
622                false,
623                type_map,
624                qualified,
625                unqualified,
626            );
627            for arg in &call.arguments {
628                collect_field_reads_impl(ast_context, arg, false, type_map, qualified, unqualified);
629            }
630        }
631        Expression::AllocCall(alloc) => {
632            collect_field_reads_impl(
633                ast_context,
634                &alloc.function,
635                false,
636                type_map,
637                qualified,
638                unqualified,
639            );
640            for arg in &alloc.arguments {
641                collect_field_reads_impl(ast_context, arg, false, type_map, qualified, unqualified);
642            }
643        }
644        Expression::Binary(b) => {
645            collect_field_reads_impl(
646                ast_context,
647                &b.left,
648                false,
649                type_map,
650                qualified,
651                unqualified,
652            );
653            collect_field_reads_impl(
654                ast_context,
655                &b.right,
656                false,
657                type_map,
658                qualified,
659                unqualified,
660            );
661        }
662        Expression::Unary(u) => {
663            collect_field_reads_impl(
664                ast_context,
665                &u.operand,
666                false,
667                type_map,
668                qualified,
669                unqualified,
670            );
671        }
672        Expression::Update(u) => {
673            collect_field_reads_impl(
674                ast_context,
675                &u.operand,
676                false,
677                type_map,
678                qualified,
679                unqualified,
680            );
681        }
682        Expression::Cast(c) => {
683            collect_field_reads_impl(
684                ast_context,
685                &c.operand,
686                false,
687                type_map,
688                qualified,
689                unqualified,
690            );
691        }
692        Expression::Conditional(c) => {
693            collect_field_reads_impl(
694                ast_context,
695                &c.condition,
696                false,
697                type_map,
698                qualified,
699                unqualified,
700            );
701            collect_field_reads_impl(
702                ast_context,
703                &c.then_expr,
704                false,
705                type_map,
706                qualified,
707                unqualified,
708            );
709            collect_field_reads_impl(
710                ast_context,
711                &c.else_expr,
712                false,
713                type_map,
714                qualified,
715                unqualified,
716            );
717        }
718        Expression::Subscript(s) => {
719            collect_field_reads_impl(
720                ast_context,
721                &s.array,
722                false,
723                type_map,
724                qualified,
725                unqualified,
726            );
727            collect_field_reads_impl(
728                ast_context,
729                &s.index,
730                false,
731                type_map,
732                qualified,
733                unqualified,
734            );
735        }
736        Expression::Identifier(_)
737        | Expression::StringLiteral(_)
738        | Expression::NumberLiteral(_)
739        | Expression::Null(_)
740        | Expression::Boolean(_)
741        | Expression::Sizeof(_)
742        | Expression::CharLiteral(_)
743        | Expression::Comment(_)
744        | Expression::OffsetOf(_)
745        | Expression::Generic(_) => {}
746    }
747}
748
749fn extract_field_refs_from_text(text: &str, unqualified: &mut HashSet<String>) {
750    let b = text.as_bytes();
751    let mut i = 0;
752    while i < b.len() {
753        let arrow = i + 1 < b.len() && b[i] == b'-' && b[i + 1] == b'>';
754        let dot = b[i] == b'.'
755            && i > 0
756            && (b[i - 1].is_ascii_alphanumeric()
757                || b[i - 1] == b'_'
758                || matches!(b[i - 1], b')' | b']'));
759        if arrow || dot {
760            let start = i + if arrow { 2 } else { 1 };
761            let mut end = start;
762            while end < b.len() && (b[end].is_ascii_alphanumeric() || b[end] == b'_') {
763                end += 1;
764            }
765            if end > start {
766                unqualified.insert(text[start..end].to_owned());
767            }
768            i = end;
769        } else {
770            i += 1;
771        }
772    }
773}
774
775impl DeadCode {
776    fn report_function_violations(
777        &self,
778        ast_context: &AstContext,
779        func_defs: &FuncDefMap,
780        func_decls: &FuncDeclMap,
781        func_refs: &HashSet<String>,
782        violations: &mut Vec<Violation>,
783    ) {
784        for (&func_name, defs) in func_defs {
785            if func_refs.contains(func_name) {
786                continue;
787            }
788            for (def_path, is_static, location) in defs {
789                if *is_static {
790                    violations.push(self.violation_at(
791                        def_path,
792                        location,
793                        format!("Static function '{}' is never used", func_name),
794                    ));
795                    continue;
796                }
797                if let Some(decls) = func_decls.get(func_name) {
798                    if decls
799                        .iter()
800                        .any(|(p, _)| ast_context.is_public_header(p) == Some(true))
801                    {
802                        continue;
803                    }
804                    for (decl_path, decl_location) in decls {
805                        violations.push(self.violation_at(
806                            decl_path,
807                            decl_location,
808                            format!(
809                                "Internal function '{}' is never used (declared in private header)",
810                                func_name
811                            ),
812                        ));
813                    }
814                }
815            }
816        }
817
818        for (&func_name, decls) in func_decls {
819            if func_refs.contains(func_name) || func_defs.contains_key(func_name) {
820                continue;
821            }
822            if decls
823                .iter()
824                .any(|(p, _)| ast_context.is_public_header(p) == Some(true))
825            {
826                continue;
827            }
828            for (decl_path, decl_location) in decls {
829                violations.push(self.violation_at(
830                    decl_path,
831                    decl_location,
832                    format!(
833                        "Internal function '{}' is never used (declared but not defined)",
834                        func_name
835                    ),
836                ));
837            }
838        }
839    }
840
841    fn report_type_violations(
842        &self,
843        ast_context: &AstContext,
844        type_defs: &TypeDefMap,
845        type_refs: &HashSet<String>,
846        violations: &mut Vec<Violation>,
847    ) {
848        for (&type_name, defs) in type_defs {
849            if ast_context
850                .type_aliases()
851                .is_referenced(type_name, type_refs)
852            {
853                continue;
854            }
855            for (def_path, location) in defs {
856                violations.push(self.violation_at(
857                    def_path,
858                    location,
859                    format!("Type '{}' is defined but never used", type_name),
860                ));
861            }
862        }
863    }
864
865    fn report_enum_value_violations(
866        &self,
867        enum_value_defs: &EnumValueDefMap,
868        func_refs: &HashSet<String>,
869        violations: &mut Vec<Violation>,
870    ) {
871        for (&value_name, defs) in enum_value_defs {
872            if func_refs.contains(value_name) {
873                continue;
874            }
875            for (def_path, location) in defs {
876                violations.push(self.violation_at(
877                    def_path,
878                    location,
879                    format!("Enum value '{}' is defined but never used", value_name),
880                ));
881            }
882        }
883    }
884
885    fn report_field_violations(
886        &self,
887        ast_context: &AstContext,
888        field_defs: &FieldDefMap,
889        field_refs_qualified: &HashMap<String, HashSet<String>>,
890        field_refs_unqualified: &HashSet<String>,
891        violations: &mut Vec<Violation>,
892    ) {
893        for (&field_name, defs) in field_defs {
894            if field_refs_unqualified.contains(field_name) {
895                continue;
896            }
897            for (def_path, location, struct_name) in defs {
898                if ast_context.type_aliases().field_is_referenced(
899                    struct_name,
900                    field_name,
901                    field_refs_qualified,
902                ) {
903                    continue;
904                }
905                violations.push(self.violation_at(
906                    def_path,
907                    location,
908                    format!("Field '{}' in '{}' is never read", field_name, struct_name),
909                ));
910            }
911        }
912    }
913}