vhdl_lang/lint/
dead_code.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this file,
3// You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) 2023, Olof Kraigher olof.kraigher@gmail.com
6
7use crate::analysis::DesignRoot;
8use crate::analysis::Library;
9use crate::analysis::LockedUnit;
10use crate::ast::search::Search;
11use crate::ast::search::SearchState;
12use crate::ast::search::Searcher;
13use crate::ast::UnitId;
14use crate::data::error_codes::ErrorCode;
15use crate::data::DiagnosticHandler;
16use crate::data::Symbol;
17use crate::named_entity::{HasEntityId, Reference, Related};
18use crate::syntax::TokenAccess;
19use crate::AnyEntKind;
20use crate::Config;
21use crate::Design;
22use crate::Diagnostic;
23use crate::EntRef;
24use crate::Overloaded;
25use crate::SrcPos;
26use fnv::FnvHashMap;
27use fnv::FnvHashSet;
28use itertools::Itertools;
29
30struct DeadCodeSearcher<'a> {
31    root: &'a DesignRoot,
32    references: FnvHashSet<EntRef<'a>>,
33    declarations: FnvHashSet<EntRef<'a>>,
34}
35
36impl<'a> DeadCodeSearcher<'a> {
37    fn new(root: &'a DesignRoot) -> Self {
38        DeadCodeSearcher {
39            root,
40            references: Default::default(),
41            declarations: Default::default(),
42        }
43    }
44}
45
46impl Searcher for DeadCodeSearcher<'_> {
47    fn search_pos_with_ref(
48        &mut self,
49        _ctx: &dyn TokenAccess,
50        _: &SrcPos,
51        reference: &Reference,
52    ) -> SearchState {
53        if let Some(id) = reference.get() {
54            let ent = self.root.get_ent(id);
55            self.references.insert(ent);
56
57            if let Related::DeclaredBy(other) = ent.related {
58                self.references.insert(other);
59            }
60        };
61        SearchState::NotFinished
62    }
63    fn search_decl(
64        &mut self,
65        _ctx: &dyn TokenAccess,
66        decl: crate::ast::search::FoundDeclaration<'_>,
67    ) -> SearchState {
68        if let Some(id) = decl.ent_id() {
69            self.declarations.insert(self.root.get_ent(id));
70        }
71        SearchState::NotFinished
72    }
73}
74
75fn search_unit(unit: &LockedUnit, searcher: &mut impl Searcher) {
76    let _ = unit.unit.write().search(&unit.tokens, searcher);
77}
78
79fn is_package_header(ent: EntRef<'_>) -> bool {
80    matches!(
81        ent.kind(),
82        AnyEntKind::Design(Design::Package(..)) | AnyEntKind::Design(Design::UninstPackage(..))
83    )
84}
85
86fn is_interface(ent: EntRef<'_>) -> bool {
87    matches!(
88        ent.kind(),
89        AnyEntKind::Object(o) if o.iface.is_some())
90        || matches!(
91            ent.kind(),
92            AnyEntKind::Overloaded(Overloaded::InterfaceSubprogram(..))
93        )
94}
95
96fn can_be_locally_unused(ent: EntRef<'_>) -> bool {
97    if let Related::DeclaredBy(related) = ent.related {
98        if !can_be_locally_unused(related) {
99            return false;
100        }
101    }
102
103    if matches!(ent.kind(), AnyEntKind::Design(_)) {
104        return false;
105    }
106
107    // No labels
108    if matches!(
109        ent.kind(),
110        AnyEntKind::Concurrent(..) | AnyEntKind::Sequential(_)
111    ) {
112        return false;
113    }
114
115    // No loop parameters
116    if matches!(ent.kind(), AnyEntKind::LoopParameter(..)) {
117        return false;
118    }
119
120    // Record elements can never be assumed to be unused, they can be created by (other => 0)
121    if matches!(ent.kind(), AnyEntKind::ElementDeclaration(..)) {
122        return false;
123    }
124
125    // Enum variants can never be assumed to be unused, they can be created by enum_t'val(0)
126    if matches!(
127        ent.kind(),
128        AnyEntKind::Overloaded(Overloaded::EnumLiteral(..))
129    ) {
130        return false;
131    }
132
133    if let Some(parent) = ent.parent {
134        // Everything in package header is public, except generics in an uninstantiated package
135        if is_package_header(parent) && !is_interface(ent) {
136            return false;
137        }
138
139        // Component ports are assumed to be needed
140        if matches!(parent.kind(), AnyEntKind::Component(..)) {
141            return false;
142        }
143
144        // Everything in protected types inside of package header
145        if matches!(parent.kind(), AnyEntKind::Type(crate::Type::Protected(..))) {
146            if let Some(grand_parent) = parent.parent {
147                if is_package_header(grand_parent) {
148                    return false;
149                }
150            }
151        }
152
153        // Formals inside a subprogram declaration
154        if matches!(
155            parent.kind(),
156            AnyEntKind::Overloaded(Overloaded::SubprogramDecl(..))
157        ) {
158            return false;
159        }
160    }
161
162    true
163}
164
165/// Find *local* unused declarations
166fn find_unused_declarations<'a>(
167    root: &'a DesignRoot,
168    lib: &Library,
169    primary_unit_name: &Symbol,
170) -> FnvHashSet<EntRef<'a>> {
171    let mut searcher = DeadCodeSearcher::new(root);
172
173    if let Some(unit) = lib.primary_unit(primary_unit_name) {
174        search_unit(unit, &mut searcher);
175    }
176
177    for unit in lib.secondary_units(primary_unit_name) {
178        search_unit(unit, &mut searcher);
179    }
180
181    searcher
182        .declarations
183        .difference(&searcher.references)
184        .filter(|ent| {
185            if let Related::DeclaredBy(other) = ent.related {
186                // If the subprogram header is found in the references
187                // but not the body. The body is considered to be used and we filter it away
188                !searcher.references.contains(other)
189            } else {
190                true
191            }
192        })
193        .filter(|ent| can_be_locally_unused(ent))
194        .copied()
195        .collect()
196}
197
198/// Use a struct to keep state of units that do not need to be re-scanned
199#[derive(Default)]
200pub(crate) struct UnusedDeclarationsLinter {
201    // library name, primary name
202    diagnostics: FnvHashMap<(Symbol, Symbol), Vec<Diagnostic>>,
203}
204
205impl UnusedDeclarationsLinter {
206    pub fn lint(
207        &mut self,
208        root: &DesignRoot,
209        config: &Config,
210        analyzed_units: &[UnitId],
211        diagnostics: &mut dyn DiagnosticHandler,
212    ) {
213        // Prune diagnostics that need to be re-computed
214        for unit in analyzed_units {
215            let key = (unit.library_name().clone(), unit.primary_name().clone());
216            self.diagnostics.remove(&key);
217        }
218
219        // Prune diagnostics for units that no longer exist
220        self.diagnostics.retain(|(library_name, primary_name), _| {
221            if let Some(library) = root.get_lib(library_name) {
222                if library.primary_unit(primary_name).is_some() {
223                    return true;
224                }
225            }
226            false
227        });
228
229        for unit in analyzed_units {
230            let key = (unit.library_name().clone(), unit.primary_name().clone());
231
232            if let Some(library) = root.get_lib(unit.library_name()) {
233                self.diagnostics.entry(key).or_insert_with(|| {
234                    find_unused_declarations(root, library, unit.primary_name())
235                        .into_iter()
236                        .filter_map(|ent| {
237                            Some(Diagnostic::new(
238                                ent.decl_pos()?,
239                                format!("Unused declaration of {}", ent.describe()),
240                                ErrorCode::Unused,
241                            ))
242                        })
243                        .collect_vec()
244                });
245            }
246        }
247
248        for ((library_name, _), unit_diagnostics) in self.diagnostics.iter() {
249            if let Some(library_config) = config.get_library(&library_name.name_utf8()) {
250                if !library_config.is_third_party {
251                    diagnostics.append(unit_diagnostics.iter().cloned());
252                }
253            }
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use crate::analysis::tests::LibraryBuilder;
262    use crate::syntax::test::check_no_diagnostics;
263    use crate::syntax::test::Code;
264
265    fn get_ent(root: &DesignRoot, code: Code) -> EntRef<'_> {
266        root.search_reference(code.source(), code.start()).unwrap()
267    }
268
269    fn check_unused(got: FnvHashSet<EntRef<'_>>, expected: FnvHashSet<EntRef<'_>>) {
270        fn fmt_ent(ent: EntRef<'_>) -> String {
271            format!(
272                "{}, line {}",
273                ent.describe(),
274                ent.decl_pos().unwrap().start().line
275            )
276        }
277
278        let mut fail = false;
279        for ent in expected.difference(&got) {
280            println!("Expected {}", fmt_ent(ent));
281            fail = true;
282        }
283        for ent in got.difference(&expected) {
284            println!("Got unexpected {}", fmt_ent(ent));
285            fail = true;
286        }
287
288        if fail {
289            panic!("Check unused failed");
290        }
291    }
292
293    #[test]
294    fn unused_signal() {
295        let mut builder = LibraryBuilder::new();
296
297        let code = builder.code(
298            "libname",
299            "
300entity ent is
301end entity;
302
303architecture a of ent is
304  signal unused : boolean;
305  signal used : boolean;
306begin
307   used <= true;
308end architecture;",
309        );
310
311        let (root, diagnostics) = builder.get_analyzed_root();
312        check_no_diagnostics(&diagnostics);
313
314        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
315        let ent = get_ent(&root, code.s1("unused"));
316
317        check_unused(
318            find_unused_declarations(&root, lib, &root.symbol_utf8("ent")),
319            FnvHashSet::from_iter(vec![ent]),
320        )
321    }
322
323    #[test]
324    fn unused_ports() {
325        let mut builder = LibraryBuilder::new();
326
327        let code = builder.code(
328            "libname",
329            "
330entity ent is
331  port (
332    used : out boolean;
333    unused : out boolean
334  );
335end entity;
336
337architecture a of ent is
338begin
339   used <= true;
340end architecture;",
341        );
342
343        let (root, diagnostics) = builder.get_analyzed_root();
344        check_no_diagnostics(&diagnostics);
345
346        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
347        let ent = get_ent(&root, code.s1("unused"));
348
349        check_unused(
350            find_unused_declarations(&root, lib, &root.symbol_utf8("ent")),
351            FnvHashSet::from_iter(vec![ent]),
352        )
353    }
354
355    /// Since the focus of the unused declaration lint is local declarations
356    /// we have to assume that a package header declaration could be used somewhere else.
357    #[test]
358    fn package_headers_are_public_and_will_never_be_unused() {
359        let mut builder = LibraryBuilder::new();
360
361        let code = builder.code(
362            "libname",
363            "
364package pkg is
365  constant unknown : boolean := false;
366end package;
367
368package body pkg is
369   constant unused : boolean := false;
370end package body;
371            ",
372        );
373
374        let (root, diagnostics) = builder.get_analyzed_root();
375        check_no_diagnostics(&diagnostics);
376
377        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
378        let ent = get_ent(&root, code.s1("unused"));
379
380        check_unused(
381            find_unused_declarations(&root, lib, &root.symbol_utf8("pkg")),
382            FnvHashSet::from_iter(vec![ent]),
383        )
384    }
385
386    #[test]
387    fn generic_package_headers_are_public_and_will_never_be_unused() {
388        let mut builder = LibraryBuilder::new();
389
390        let code = builder.code(
391            "libname",
392            "
393package pkg is
394  generic (arg : boolean; unused0 : natural);
395  constant unknown : boolean := arg;
396end package;
397
398package body pkg is
399   constant unused1 : boolean := false;
400end package body;
401            ",
402        );
403
404        let (root, diagnostics) = builder.get_analyzed_root();
405        check_no_diagnostics(&diagnostics);
406
407        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
408
409        check_unused(
410            find_unused_declarations(&root, lib, &root.symbol_utf8("pkg")),
411            FnvHashSet::from_iter(vec![
412                get_ent(&root, code.s1("unused0")),
413                get_ent(&root, code.s1("unused1")),
414            ]),
415        )
416    }
417
418    // We can never assume enum variants are unused because they can be created by enum_t'val(0)
419    #[test]
420    fn enum_variants_are_never_unused() {
421        let mut builder = LibraryBuilder::new();
422
423        let code = builder.code(
424            "libname",
425            "
426package pkg is
427    type enum_pub_t is (v1, v2);
428end package;
429
430package body pkg is
431    type enum_priv_t is (v3, v4);
432end package body;
433            ",
434        );
435
436        let (root, diagnostics) = builder.get_analyzed_root();
437        check_no_diagnostics(&diagnostics);
438
439        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
440
441        check_unused(
442            find_unused_declarations(&root, lib, &root.symbol_utf8("pkg")),
443            FnvHashSet::from_iter(vec![get_ent(&root, code.s1("enum_priv_t"))]),
444        )
445    }
446
447    // We can never assume record elements are unused because they can be created by (others => 0)
448    #[test]
449    fn record_elements_are_never_unused() {
450        let mut builder = LibraryBuilder::new();
451
452        let code = builder.code(
453            "libname",
454            "
455package pkg is
456    type rec_pub_t is record
457       elem : natural;
458    end record;
459end package;
460
461package body pkg is
462    type rec_priv_t is record
463        elem : natural;
464    end record;
465end package body;
466            ",
467        );
468
469        let (root, diagnostics) = builder.get_analyzed_root();
470        check_no_diagnostics(&diagnostics);
471
472        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
473
474        check_unused(
475            find_unused_declarations(&root, lib, &root.symbol_utf8("pkg")),
476            FnvHashSet::from_iter(vec![get_ent(&root, code.s1("rec_priv_t"))]),
477        )
478    }
479
480    #[test]
481    fn protected_type_methods_are_indirectly_public() {
482        let mut builder = LibraryBuilder::new();
483
484        builder.code(
485            "libname",
486            "
487package pkg is
488  type prot_t is protected
489     procedure method;
490  end protected;
491end package;
492
493package body pkg is
494    type prot_t is protected body
495      procedure method is
496      begin
497      end;
498    end protected body;
499end package body;
500            ",
501        );
502
503        let (root, diagnostics) = builder.get_analyzed_root();
504        check_no_diagnostics(&diagnostics);
505
506        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
507
508        check_unused(
509            find_unused_declarations(&root, lib, &root.symbol_utf8("pkg")),
510            FnvHashSet::default(),
511        )
512    }
513
514    #[test]
515    fn labels_are_not_unused() {
516        let mut builder = LibraryBuilder::new();
517
518        builder.code(
519            "libname",
520            "
521entity ent2 is
522end entity;
523
524architecture a of ent2 is
525begin
526end architecture;
527
528entity ent is
529end entity;
530
531architecture a of ent is
532begin
533   main : process
534   begin
535     l0: loop
536     end loop;
537   end process;
538
539   inst : entity work.ent2;
540end architecture;",
541        );
542
543        let (root, diagnostics) = builder.get_analyzed_root();
544        check_no_diagnostics(&diagnostics);
545
546        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
547        check_unused(
548            find_unused_declarations(&root, lib, &root.symbol_utf8("ent")),
549            FnvHashSet::default(),
550        )
551    }
552
553    #[test]
554    fn loop_parameters_are_not_unused() {
555        let mut builder = LibraryBuilder::new();
556
557        builder.code(
558            "libname",
559            "
560entity ent is
561end entity;
562
563architecture a of ent is
564begin
565   main : process
566   begin
567      for i in 0 to 3 loop
568      end loop;
569   end process;
570
571   gen: for j in 0 to 3 generate
572   end generate;
573
574end architecture;",
575        );
576
577        let (root, diagnostics) = builder.get_analyzed_root();
578        check_no_diagnostics(&diagnostics);
579
580        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
581        check_unused(
582            find_unused_declarations(&root, lib, &root.symbol_utf8("ent")),
583            FnvHashSet::default(),
584        )
585    }
586
587    #[test]
588    fn subprogram_declaration_arguments_are_not_unused() {
589        let mut builder = LibraryBuilder::new();
590
591        builder.code(
592            "libname",
593            "
594package pkg is
595  procedure myproc(arg : natural);
596end package;
597
598package body pkg is
599    procedure myproc(arg : natural) is
600    begin
601       report to_string(arg);
602    end;
603end package body;
604            ",
605        );
606
607        let (root, diagnostics) = builder.get_analyzed_root();
608        check_no_diagnostics(&diagnostics);
609
610        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
611
612        check_unused(
613            find_unused_declarations(&root, lib, &root.symbol_utf8("pkg")),
614            FnvHashSet::default(),
615        )
616    }
617
618    #[test]
619    fn subprogram_use_between_declaration_and_definition() {
620        let mut builder = LibraryBuilder::new();
621
622        let code = builder.code(
623            "libname",
624            "
625package pkg is
626    procedure pub_proc;
627end package;
628
629package body pkg is
630
631   procedure priv_proc;
632
633   procedure pub_proc is
634   begin
635     priv_proc;
636   end procedure;
637
638   procedure priv_proc is
639   begin
640   end;
641
642   procedure unused;
643   procedure unused is
644   begin
645   end;
646
647end package body;
648            ",
649        );
650
651        let (root, diagnostics) = builder.get_analyzed_root();
652        check_no_diagnostics(&diagnostics);
653
654        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
655
656        check_unused(
657            find_unused_declarations(&root, lib, &root.symbol_utf8("pkg")),
658            FnvHashSet::from_iter(vec![
659                get_ent(&root, code.s("unused", 1)),
660                get_ent(&root, code.s("unused", 2)),
661            ]),
662        )
663    }
664
665    #[test]
666    fn component_interface_is_not_unused() {
667        let mut builder = LibraryBuilder::new();
668
669        builder.code(
670            "libname",
671            "
672package pkg is
673  component comp is
674    generic (g : natural);
675    port (p : natural);
676  end component;
677end package;
678            ",
679        );
680
681        let (root, diagnostics) = builder.get_analyzed_root();
682        check_no_diagnostics(&diagnostics);
683
684        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
685
686        check_unused(
687            find_unused_declarations(&root, lib, &root.symbol_utf8("pkg")),
688            FnvHashSet::default(),
689        )
690    }
691
692    #[test]
693    fn protected_type_methods() {
694        let mut builder = LibraryBuilder::new();
695
696        let code = builder.code(
697            "libname",
698            "
699package pkg is
700    procedure public;
701end package;
702
703package body pkg is
704    type prot_t is protected
705        procedure method;
706
707        procedure unused;
708    end protected;
709
710    type prot_t is protected body
711        procedure method is
712        begin
713        end;
714        
715        procedure unused is
716        begin
717        end;
718    end protected body;
719
720    procedure public is
721       variable p : prot_t;
722    begin
723       p.method;
724    end;
725end package body;
726            ",
727        );
728
729        let (root, diagnostics) = builder.get_analyzed_root();
730        check_no_diagnostics(&diagnostics);
731
732        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
733
734        check_unused(
735            find_unused_declarations(&root, lib, &root.symbol_utf8("pkg")),
736            FnvHashSet::from_iter(vec![
737                get_ent(&root, code.s("unused", 1)),
738                get_ent(&root, code.s("unused", 2)),
739            ]),
740        )
741    }
742
743    #[test]
744    fn end_label_is_not_valid_use() {
745        let mut builder = LibraryBuilder::new();
746
747        let code = builder.code(
748            "libname",
749            "
750package pkg is
751end package;
752
753package body pkg is
754    procedure unused is
755    begin
756    end unused;
757end package body;
758            ",
759        );
760
761        let (root, diagnostics) = builder.get_analyzed_root();
762        check_no_diagnostics(&diagnostics);
763
764        let lib = root.get_lib(&root.symbol_utf8("libname")).unwrap();
765
766        check_unused(
767            find_unused_declarations(&root, lib, &root.symbol_utf8("pkg")),
768            FnvHashSet::from_iter(vec![get_ent(&root, code.s1("unused"))]),
769        )
770    }
771}