Skip to main content

lisette_semantics/lint/
mod.rs

1mod ast_lints;
2mod fact_lints;
3mod ref_lints;
4
5use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
6
7use crate::facts::Facts;
8use crate::store::Store;
9use diagnostics::DiagnosticSink;
10use diagnostics::LisetteDiagnostic;
11use syntax::ast::Expression;
12use syntax::program::File;
13use syntax::program::Module;
14use syntax::program::UnusedInfo;
15
16use ast_lints::AstLintGroup;
17use fact_lints::FactLintGroup;
18use ref_lints::RefLintGroup;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub enum Lint {
22    UnusedVariable,
23    UnusedParameter,
24    UnusedMut,
25    UnusedImport,
26    UnusedType,
27    UnusedFunction,
28    UnusedConstant,
29    UnusedStructField,
30    UnusedEnumVariant,
31    UnusedLiteral,
32    UnusedResult,
33    UnusedOption,
34    UnusedValue,
35    DeadCodeAfterReturn,
36    DeadCodeAfterBreak,
37    DeadCodeAfterContinue,
38    DeadCodeAfterDivergingIf,
39    DeadCodeAfterDivergingMatch,
40    DeadCodeAfterInfiniteLoop,
41    DeadCodeAfterDivergingCall,
42    DoubleBoolNegation,
43    DoubleIntNegation,
44    SelfComparison,
45    SelfAssignment,
46    MatchLiteralCollection,
47    EmptyMatchArm,
48    InternalTypeLeak,
49    UnnecessaryReference,
50    UnusedTypeParameter,
51    RestOnlySlicePattern,
52    NonPascalCaseType,
53    NonPascalCaseTypeParameter,
54    NonPascalCaseEnumVariant,
55    NonSnakeCaseFunction,
56    NonSnakeCaseVariable,
57    NonSnakeCaseParameter,
58    NonSnakeCaseStructField,
59    NonScreamingSnakeCaseConstant,
60    RedundantIfLet,
61    RedundantLetElse,
62    SingleArmMatch,
63    RedundantIfLetElse,
64    UnreachableIfLetElse,
65    TryBlockNoSuccessPath,
66    ExcessParensOnCondition,
67}
68
69#[derive(Debug, Clone, Default)]
70pub struct LintConfig {
71    disabled: HashSet<Lint>,
72}
73
74impl LintConfig {
75    pub fn is_enabled(&self, lint: Lint) -> bool {
76        !self.disabled.contains(&lint)
77    }
78}
79
80pub trait LintRule {
81    fn check(&self, ctx: &LintContext) -> Vec<LisetteDiagnostic>;
82}
83
84pub struct LintContext<'a> {
85    pub ast: &'a [Expression],
86    pub facts: &'a Facts,
87    pub module: Option<&'a Module>,
88    pub config: &'a LintConfig,
89    pub is_d_lis: bool,
90    /// Files for this module (used by ref_lints for cross-file analysis)
91    pub files: &'a HashMap<u32, File>,
92}
93
94fn all_lint_rules() -> Vec<Box<dyn LintRule>> {
95    vec![
96        Box::new(FactLintGroup),
97        Box::new(AstLintGroup),
98        Box::new(RefLintGroup),
99    ]
100}
101
102pub fn lint_all_modules(store: &Store, facts: &Facts, sink: &DiagnosticSink) -> UnusedInfo {
103    let config = LintConfig::default();
104    let mut unused = UnusedInfo::default();
105
106    // Fact lints run once globally (facts are shared across all modules)
107    {
108        let empty_ast = [];
109        let empty_files = HashMap::default();
110        let ctx = LintContext {
111            ast: &empty_ast,
112            facts,
113            module: None,
114            config: &config,
115            is_d_lis: false,
116            files: &empty_files,
117        };
118        let mut diagnostics = FactLintGroup.check(&ctx);
119        diagnostics.sort_by_key(|d| d.primary_offset());
120        for diagnostic in diagnostics {
121            sink.push(diagnostic);
122        }
123    }
124
125    for module in store.modules.values() {
126        if module.is_internal() {
127            continue;
128        }
129        lint_module(module, facts, &config, sink, &mut unused);
130    }
131
132    for b in facts.bindings.values() {
133        if !b.used {
134            unused.mark_binding_unused(b.span);
135        }
136    }
137
138    unused
139}
140
141fn lint_module(
142    module: &Module,
143    facts: &Facts,
144    config: &LintConfig,
145    sink: &DiagnosticSink,
146    unused: &mut UnusedInfo,
147) {
148    // Module-level lints (reference graph analysis)
149    let ref_result = ref_lints::run_ref_lints(module, &module.files, config, facts);
150    if !ref_result.unused_import_aliases.is_empty() {
151        unused.imports_by_module.insert(
152            module.id.clone().into(),
153            ref_result
154                .unused_import_aliases
155                .into_iter()
156                .map(|s| s.into())
157                .collect(),
158        );
159    }
160    for span in ref_result.unused_definition_spans {
161        unused.mark_definition_unused(span);
162    }
163    for diagnostic in ref_result.diagnostics {
164        sink.push(diagnostic);
165    }
166
167    // File-level lints
168    for file in module.files.values() {
169        let ctx = LintContext {
170            ast: &file.items,
171            facts,
172            module: Some(module),
173            config,
174            is_d_lis: file.is_d_lis(),
175            files: &module.files,
176        };
177
178        let mut diagnostics = AstLintGroup.check(&ctx);
179        diagnostics.sort_by_key(|d| d.primary_offset());
180        for diagnostic in diagnostics {
181            sink.push(diagnostic);
182        }
183    }
184}
185
186pub fn lint_file(ctx: &LintContext, sink: &DiagnosticSink) {
187    let mut diagnostics: Vec<_> = all_lint_rules()
188        .iter()
189        .flat_map(|lint| lint.check(ctx))
190        .collect();
191
192    diagnostics.sort_by_key(|d| d.primary_offset());
193
194    for diagnostic in diagnostics {
195        sink.push(diagnostic);
196    }
197}