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