Skip to main content

lisette_diagnostics/
lint.rs

1use crate::LisetteDiagnostic;
2use syntax::ast::{DeadCodeCause, Span};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum IssueKind {
6    RedundantLetElse,
7    RedundantIfLet,
8    UnreachableIfLetElse,
9    RedundantIfLetElse,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum UnusedExpressionKind {
14    Literal,
15    Result,
16    Option,
17    Value,
18}
19
20impl UnusedExpressionKind {
21    pub fn lint_name(&self) -> &'static str {
22        match self {
23            Self::Literal => "unused_literal",
24            Self::Result => "unused_result",
25            Self::Option => "unused_option",
26            Self::Value => "unused_value",
27        }
28    }
29}
30
31pub fn unused_variable(span: &Span, name: &str, is_struct_field: bool) -> LisetteDiagnostic {
32    let help = if is_struct_field {
33        format!(
34            "Use this variable or prefix it with an underscore: `{}: _{}`.",
35            name, name
36        )
37    } else {
38        format!(
39            "Use this variable or prefix it with an underscore: `_{}`.",
40            name
41        )
42    };
43    LisetteDiagnostic::warn("Unused variable")
44        .with_lint_code("unused_variable")
45        .with_span_label(span, "never used")
46        .with_help(help)
47}
48
49pub fn unused_parameter(span: &Span, name: &str) -> LisetteDiagnostic {
50    LisetteDiagnostic::warn("Unused parameter")
51        .with_lint_code("unused_param")
52        .with_span_label(span, "never used")
53        .with_help(format!(
54            "Use this parameter or prefix it with an underscore: `_{}`.",
55            name
56        ))
57}
58
59pub fn unused_mut(span: &Span) -> LisetteDiagnostic {
60    LisetteDiagnostic::warn("Unused `mut`")
61        .with_lint_code("unnecessary_mut")
62        .with_span_label(span, "declared as mutable")
63        .with_help("Remove `mut` from the declaration if you do not need to mutate the variable")
64}
65
66pub fn written_but_not_read(span: &Span, name: &str) -> LisetteDiagnostic {
67    LisetteDiagnostic::warn("Variable assigned but never read")
68        .with_lint_code("assigned_but_never_read")
69        .with_span_label(span, format!("`{}` is assigned but never read", name))
70        .with_help(
71            "Read the variable after assigning it, or explicitly discard it with `let _ = ...`",
72        )
73}
74
75pub fn dead_code(span: &Span, cause: DeadCodeCause) -> LisetteDiagnostic {
76    let (code, msg) = match cause {
77        DeadCodeCause::Return => ("dead_code_after_return", "Unreachable code after return"),
78        DeadCodeCause::Break => ("dead_code_after_break", "Unreachable code after break"),
79        DeadCodeCause::Continue => (
80            "dead_code_after_continue",
81            "Unreachable code after continue",
82        ),
83        DeadCodeCause::DivergingIf => (
84            "dead_code_after_diverging_if",
85            "Unreachable code after diverging if/else",
86        ),
87        DeadCodeCause::DivergingMatch => (
88            "dead_code_after_diverging_match",
89            "Unreachable code after diverging match",
90        ),
91        DeadCodeCause::InfiniteLoop => (
92            "dead_code_after_infinite_loop",
93            "Unreachable code after infinite loop",
94        ),
95        DeadCodeCause::DivergingCall => (
96            "dead_code_after_diverging_call",
97            "Unreachable code after diverging function call",
98        ),
99    };
100    LisetteDiagnostic::warn(msg)
101        .with_lint_code(code)
102        .with_span_label(span, "unreachable from this point onward")
103        .with_help("Remove this line and all code after it")
104}
105
106pub fn pattern_issue(span: &Span, kind: IssueKind) -> LisetteDiagnostic {
107    let (code, message, label, help) = match kind {
108        IssueKind::RedundantLetElse => (
109            "redundant_let_else",
110            "Redundant `else` in `let...else`",
111            "always matches",
112            "Remove the `else` block since the pattern cannot fail",
113        ),
114        IssueKind::RedundantIfLet => (
115            "redundant_if_let",
116            "Redundant `if let` pattern",
117            "always matches",
118            "Use `let` instead of `if let` since the pattern cannot fail",
119        ),
120        IssueKind::UnreachableIfLetElse => (
121            "unreachable_if_let_else",
122            "Unreachable `else` branch",
123            "this branch can never execute",
124            "Remove the `else` branch since the pattern always matches",
125        ),
126        IssueKind::RedundantIfLetElse => (
127            "redundant_if_let_else",
128            "Redundant `else` branch",
129            "this branch does nothing",
130            "Remove the `else` branch",
131        ),
132    };
133
134    LisetteDiagnostic::warn(message)
135        .with_lint_code(code)
136        .with_span_label(span, label)
137        .with_help(help)
138}
139
140pub fn discarded_result_in_tail(span: &Span, return_type: &str) -> LisetteDiagnostic {
141    LisetteDiagnostic::warn("`Result` is silently discarded")
142        .with_lint_code("unused_result")
143        .with_span_label(span, "failure will go unnoticed")
144        .with_help(format!(
145            "Handle this `Result` with `?` or `match`, explicitly discard it with `let _ = ...`, or return it by adding `-> {}` to the function signature",
146            return_type
147        ))
148}
149
150pub fn discarded_option_in_tail(span: &Span, return_type: &str) -> LisetteDiagnostic {
151    LisetteDiagnostic::warn("Unused Option")
152        .with_lint_code("unused_option")
153        .with_span_label(span, "this `Option` is discarded")
154        .with_help(format!(
155            "Handle this `Option`, explicitly discard it with `let _ = ...`, or return it by adding `-> {}` to the function signature",
156            return_type
157        ))
158}
159
160pub fn unused_expression(span: &Span, kind: UnusedExpressionKind) -> LisetteDiagnostic {
161    let (code, msg, label, help) = match kind {
162        UnusedExpressionKind::Literal => (
163            "unused_literal",
164            "Unused literal",
165            "this literal has no effect",
166            "Remove this literal",
167        ),
168        UnusedExpressionKind::Result => (
169            "unused_result",
170            "`Result` is silently discarded",
171            "failure will go unnoticed",
172            "Handle this `Result` with `?` or `match`, or explicitly discard it with `let _ = ...`",
173        ),
174        UnusedExpressionKind::Option => (
175            "unused_option",
176            "Unused Option",
177            "this `Option` is discarded",
178            "Handle this `Option`, or explicitly discard it with `let _ = ...`",
179        ),
180        UnusedExpressionKind::Value => (
181            "unused_value",
182            "Unused expression value",
183            "this value is discarded",
184            "Use the value, or ignore with `let _ = ...`",
185        ),
186    };
187    LisetteDiagnostic::warn(msg)
188        .with_lint_code(code)
189        .with_span_label(span, label)
190        .with_help(help)
191}
192
193pub fn unnecessary_reference(span: &Span, name: Option<&str>) -> LisetteDiagnostic {
194    let (label, help) = match name {
195        Some(n) => (
196            format!("`{}` is already a reference", n),
197            format!("Remove the `&` operator from `{}`", n),
198        ),
199        None => (
200            "value is already a reference".to_string(),
201            "Remove the `&` operator".to_string(),
202        ),
203    };
204    LisetteDiagnostic::warn("Unnecessary `&`")
205        .with_lint_code("unnecessary_reference")
206        .with_span_label(span, label)
207        .with_help(help)
208}
209
210pub fn unused_type_parameter(span: &Span) -> LisetteDiagnostic {
211    LisetteDiagnostic::warn("Unused type parameter")
212        .with_lint_code("unused_type_param")
213        .with_span_label(span, "never used")
214        .with_help("Remove the unused type parameter or use it in the signature")
215}
216
217pub fn ineffective_try_block(span: &Span) -> LisetteDiagnostic {
218    LisetteDiagnostic::warn("Ineffective `try` block")
219        .with_lint_code("try_block_no_success_path")
220        .with_span_label(span, "always propagates")
221        .with_help("A `try` block is effective only if the expression may succeed or fail")
222}
223
224pub fn double_negation(span: &Span, is_bool: bool) -> LisetteDiagnostic {
225    let (code, msg) = if is_bool {
226        ("double_bool_negation", "double boolean negation")
227    } else {
228        ("double_int_negation", "double numeric negation")
229    };
230
231    LisetteDiagnostic::warn(msg)
232        .with_lint_code(code)
233        .with_span_label(span, "mistaken double negation")
234        .with_help("Remove one of the negation operators")
235}
236
237pub fn tautological_comparison(span: &Span, always_true: bool) -> LisetteDiagnostic {
238    let result = if always_true { "true" } else { "false" };
239
240    LisetteDiagnostic::warn("Tautological comparison")
241        .with_lint_code("self_comparison")
242        .with_span_label(span, "comparing to itself")
243        .with_help(format!(
244            "This condition is always {}. Did you mean to compare different values?",
245            result
246        ))
247}
248
249pub fn self_assignment(span: &Span) -> LisetteDiagnostic {
250    LisetteDiagnostic::warn("Self-assignment")
251        .with_lint_code("self_assignment")
252        .with_span_label(span, "assigning to itself")
253        .with_help("Correct this assignment")
254}
255
256pub fn empty_match_arm(span: &Span) -> LisetteDiagnostic {
257    LisetteDiagnostic::warn("Empty match arm")
258        .with_lint_code("empty_match_arm")
259        .with_span_label(span, "forgotten stub?")
260        .with_help("Return `()` to indicate an intentional no-op in a match arm")
261}
262
263pub fn unnecessary_parens(span: &Span, keyword: &str) -> LisetteDiagnostic {
264    LisetteDiagnostic::warn("Unnecessary parens")
265        .with_lint_code("excess_parens_on_condition")
266        .with_span_label(span, "remove parens")
267        .with_help(format!(
268            "Lisette does not require parens around `{}` conditions",
269            keyword
270        ))
271}
272
273pub fn match_on_literal(span: &Span) -> LisetteDiagnostic {
274    LisetteDiagnostic::warn("Ineffective match")
275        .with_lint_code("match_on_literal")
276        .with_span_label(span, "already known")
277        .with_help(
278            "Matching on a literal is ineffective, because this always succeeds. Did you mean to match on a variable?",
279        )
280}
281
282pub fn single_arm_match(span: &Span, pattern_suggestion: &str) -> LisetteDiagnostic {
283    LisetteDiagnostic::warn("Ineffective match")
284        .with_lint_code("single_arm_match")
285        .with_span_label(span, "should be `if let`")
286        .with_help(format!(
287            "A match with a single meaningful arm is ineffective. Use `if let {} = value {{ ... }}` instead.",
288            pattern_suggestion
289        ))
290}
291
292pub fn uninterpolated_fstring(span: &Span) -> LisetteDiagnostic {
293    LisetteDiagnostic::warn("Uninterpolated f-string")
294        .with_lint_code("uninterpolated_fstring")
295        .with_span_label(span, "zero interpolations")
296        .with_help("Remove the `f` prefix. A string without interpolations does not need to be a format string")
297}
298
299pub fn expression_only_fstring(span: &Span) -> LisetteDiagnostic {
300    LisetteDiagnostic::warn("Expression-only f-string")
301        .with_lint_code("expression_only_fstring")
302        .with_span_label(span, "the entire f-string is an expression")
303        .with_help("Use the expression directly. Wrapping it in an f-string adds no value")
304}
305
306pub fn rest_only_slice_pattern(span: &Span, help: impl Into<String>) -> LisetteDiagnostic {
307    LisetteDiagnostic::warn("Ineffective pattern")
308        .with_lint_code("rest_only_slice_pattern")
309        .with_span_label(span, "always matches")
310        .with_help(help)
311}
312
313pub fn miscased_pascal(span: &Span, code: &str, suggested_name: &str) -> LisetteDiagnostic {
314    LisetteDiagnostic::warn("Miscased name")
315        .with_lint_code(code)
316        .with_span_label(span, "expected PascalCase")
317        .with_help(format!("Rename to `{}`", suggested_name))
318}
319
320pub fn miscased_snake(span: &Span, code: &str, suggested_name: &str) -> LisetteDiagnostic {
321    LisetteDiagnostic::warn("Miscased name")
322        .with_lint_code(code)
323        .with_span_label(span, "expected snake_case")
324        .with_help(format!("Rename to `{}`", suggested_name))
325}
326
327pub fn miscased_screaming_snake(span: &Span, suggested_name: &str) -> LisetteDiagnostic {
328    LisetteDiagnostic::warn("Miscased name")
329        .with_lint_code("constant_not_screaming_snake_case")
330        .with_span_label(span, "expected SCREAMING_SNAKE_CASE")
331        .with_help(format!("Rename to `{}`", suggested_name))
332}
333
334pub fn unused_field(span: &Span) -> LisetteDiagnostic {
335    LisetteDiagnostic::warn("Unused field")
336        .with_lint_code("unused_struct_field")
337        .with_span_label(span, "never read")
338        .with_help("Use or remove this field")
339}
340
341pub fn unused_variant(span: &Span) -> LisetteDiagnostic {
342    LisetteDiagnostic::warn("Unused variant")
343        .with_lint_code("unused_enum_variant")
344        .with_span_label(span, "never constructed or matched")
345        .with_help("Use or remove this enum variant")
346}
347
348pub fn unused_import(span: &Span) -> LisetteDiagnostic {
349    LisetteDiagnostic::warn("Unused import")
350        .with_lint_code("unused_import")
351        .with_span_label(span, "never used")
352        .with_help("Use or remove this import")
353}
354
355pub fn unused_type(span: &Span) -> LisetteDiagnostic {
356    LisetteDiagnostic::warn("Unused type")
357        .with_lint_code("unused_type")
358        .with_span_label(span, "never used")
359        .with_help("Use or remove this type")
360}
361
362pub fn unused_function(span: &Span) -> LisetteDiagnostic {
363    LisetteDiagnostic::warn("Unused function")
364        .with_lint_code("unused_function")
365        .with_span_label(span, "never called")
366        .with_help("Call or remove this function")
367}
368
369pub fn unused_constant(span: &Span) -> LisetteDiagnostic {
370    LisetteDiagnostic::warn("Unused constant")
371        .with_lint_code("unused_constant")
372        .with_span_label(span, "never used")
373        .with_help("Use or remove this constant")
374}
375
376pub fn private_type_in_public_api(
377    span: Option<&Span>,
378    private_type: &str,
379    public_definition: &str,
380) -> LisetteDiagnostic {
381    let mut diagnostic = LisetteDiagnostic::warn(format!(
382        "Private type `{}` in public API",
383        private_type
384    ))
385    .with_lint_code("internal_type_leak")
386    .with_help(format!(
387        "`{}` is private but exposed by `{}`, which is public. Add `pub` to the private type or remove it from the public API",
388        private_type, public_definition
389    ));
390
391    if let Some(s) = span {
392        diagnostic = diagnostic.with_span_label(s, "private");
393    }
394
395    diagnostic
396}
397
398pub fn unknown_attribute(span: &Span, name: &str) -> LisetteDiagnostic {
399    LisetteDiagnostic::warn("Unknown attribute")
400        .with_lint_code("unknown_attribute")
401        .with_span_label(span, "not recognized")
402        .with_help(format!(
403            "`{}` is not a recognized attribute. Known attributes: `#[json]`, `#[xml]`, `#[yaml]`, `#[toml]`, `#[db]`, `#[bson]`, `#[msgpack]`, `#[mapstructure]`, `#[tag]`",
404            name
405        ))
406}
407
408pub fn field_attribute_without_struct_attribute(
409    field_span: &Span,
410    attribute_name: &str,
411) -> LisetteDiagnostic {
412    LisetteDiagnostic::error("Orphan field attribute")
413        .with_lint_code("orphan_field_attribute")
414        .with_span_label(field_span, "field has attribute but struct does not")
415        .with_help(format!(
416            "Add `#[{}]` atop the struct definition to enable field-level attributes",
417            attribute_name
418        ))
419}
420
421pub fn duplicate_tag_key(span: &Span, key: &str, first_span: &Span) -> LisetteDiagnostic {
422    LisetteDiagnostic::error("Duplicate tag")
423        .with_lint_code("duplicate_tag")
424        .with_span_label(span, "duplicate")
425        .with_span_label(first_span, "first occurrence")
426        .with_help(format!(
427            "Remove one of the `{}` attributes - each tag key may appear only once per field",
428            key
429        ))
430}
431
432pub fn conflicting_case_transforms(span: &Span) -> LisetteDiagnostic {
433    LisetteDiagnostic::error("Conflicting case transforms")
434        .with_lint_code("conflicting_case_transforms")
435        .with_span_label(span, "conflicting")
436        .with_help("Choose either `snake_case` or `camel_case`, not both")
437}
438
439pub fn tag_has_alias(span: &Span, key: &str) -> LisetteDiagnostic {
440    LisetteDiagnostic::warn("Prefer predefined tag alias")
441        .with_lint_code("tag_has_alias")
442        .with_span_label(span, "use alias instead")
443        .with_help(format!(
444            "Use `#[{}(...)]` instead of `#[tag(...)]` for better validation",
445            key
446        ))
447}
448
449pub fn unknown_tag_option(span: &Span, option: &str) -> LisetteDiagnostic {
450    LisetteDiagnostic::warn("Unknown tag option")
451        .with_lint_code("unknown_tag_option")
452        .with_span_label(span, "not recognized")
453        .with_help(format!(
454            "`{}` is not a recognized tag option. Known options: `snake_case`, `camel_case`, `omitempty`, `!omitempty`, `skip`, `string`",
455            option
456        ))
457}