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 unused_expression(span: &Span, kind: UnusedExpressionKind) -> LisetteDiagnostic {
143    let (code, msg, label, help) = match kind {
144        UnusedExpressionKind::Literal => (
145            "unused_literal",
146            "Unused literal",
147            "this literal has no effect",
148            "Remove this literal",
149        ),
150        UnusedExpressionKind::Result => (
151            "unused_result",
152            "`Result` is silently discarded",
153            "failure will go unnoticed",
154            "Handle this `Result` with `?` or `match`, or explicitly discard it with `let _ = ...`",
155        ),
156        UnusedExpressionKind::Option => (
157            "unused_option",
158            "Unused Option",
159            "this `Option` is discarded",
160            "Handle this `Option`, or explicitly discard it with `let _ = ...`",
161        ),
162        UnusedExpressionKind::Partial => (
163            "unused_partial",
164            "`Partial` is silently discarded",
165            "partial result will go unnoticed",
166            "Handle this `Partial` with `match`, or explicitly discard it with `let _ = ...`",
167        ),
168        UnusedExpressionKind::Value => (
169            "unused_value",
170            "Unused expression value",
171            "this value is discarded",
172            "Use the value, or ignore with `let _ = ...`",
173        ),
174    };
175    LisetteDiagnostic::warn(msg)
176        .with_lint_code(code)
177        .with_span_label(span, label)
178        .with_help(help)
179}
180
181pub fn unnecessary_reference(span: &Span, name: Option<&str>) -> LisetteDiagnostic {
182    let (label, help) = match name {
183        Some(n) => (
184            format!("`{}` is already a reference", n),
185            format!("Remove the `&` operator from `{}`", n),
186        ),
187        None => (
188            "value is already a reference".to_string(),
189            "Remove the `&` operator".to_string(),
190        ),
191    };
192    LisetteDiagnostic::warn("Unnecessary `&`")
193        .with_lint_code("unnecessary_reference")
194        .with_span_label(span, label)
195        .with_help(help)
196}
197
198pub fn unused_type_parameter(span: &Span) -> LisetteDiagnostic {
199    LisetteDiagnostic::warn("Unused type parameter")
200        .with_lint_code("unused_type_param")
201        .with_span_label(span, "never used")
202        .with_help("Remove the unused type parameter or use it in the signature")
203}
204
205pub fn type_param_only_in_bound(span: &Span, name: &str) -> LisetteDiagnostic {
206    LisetteDiagnostic::warn("Type parameter only used in bound")
207        .with_lint_code("type_param_only_in_bound")
208        .with_span_label(
209            span,
210            format!("`{}` is only used inside another parameter's bound", name),
211        )
212        .with_help("Remove it, or use it in a parameter type, return type, or bound left-hand side")
213}
214
215pub fn ineffective_try_block(span: &Span) -> LisetteDiagnostic {
216    LisetteDiagnostic::warn("Ineffective `try` block")
217        .with_lint_code("try_block_no_success_path")
218        .with_span_label(span, "always propagates")
219        .with_help("A `try` block is effective only if the expression may succeed or fail")
220}
221
222pub fn replaceable_with_zero_fill(span: &Span, kept: &str, struct_name: &str) -> LisetteDiagnostic {
223    let example = if kept.is_empty() {
224        format!("`{} {{ .. }}`", struct_name)
225    } else {
226        format!("`{} {{ {}, .. }}`", struct_name, kept)
227    };
228    LisetteDiagnostic::warn("Replaceable with zero-fill spread")
229        .with_lint_code("replaceable_with_zero_fill")
230        .with_span_label(span, "has zero-valued fields")
231        .with_help(format!(
232            "Replace zero-valued fields with zero-fill spread: {}",
233            example
234        ))
235}
236
237pub fn double_negation(span: &Span, is_bool: bool) -> LisetteDiagnostic {
238    let (code, msg) = if is_bool {
239        ("double_bool_negation", "Double boolean negation")
240    } else {
241        ("double_int_negation", "Double numeric negation")
242    };
243
244    LisetteDiagnostic::warn(msg)
245        .with_lint_code(code)
246        .with_span_label(span, "accidental double negation")
247        .with_help("Remove one of the negation operators")
248}
249
250pub fn tautological_comparison(span: &Span, always_true: bool) -> LisetteDiagnostic {
251    let result = if always_true { "true" } else { "false" };
252
253    LisetteDiagnostic::warn("Tautological comparison")
254        .with_lint_code("self_comparison")
255        .with_span_label(span, "comparing to itself")
256        .with_help(format!(
257            "This condition is always {}. Did you mean to compare different values?",
258            result
259        ))
260}
261
262pub fn unsigned_comparison(span: &Span, always_true: bool) -> LisetteDiagnostic {
263    let result = if always_true { "true" } else { "false" };
264
265    LisetteDiagnostic::warn(format!("Comparison is always {result}"))
266        .with_lint_code("unsigned_comparison")
267        .with_span_label(span, format!("always {result}"))
268        .with_help(
269            "An unsigned integer is never negative, so this comparison always has the same result. Did you mean to compare against a different value?",
270        )
271}
272
273pub fn verbose_failure_propagation(span: &Span) -> LisetteDiagnostic {
274    LisetteDiagnostic::warn("Verbose failure propagation")
275        .with_lint_code("verbose_failure_propagation")
276        .with_span_label(span, "verbose")
277        .with_help("Use `?` to propagate the failure concisely")
278}
279
280pub fn self_assignment(span: &Span) -> LisetteDiagnostic {
281    LisetteDiagnostic::warn("Self-assignment")
282        .with_lint_code("self_assignment")
283        .with_span_label(span, "assigning to itself")
284        .with_help("Correct this assignment")
285}
286
287pub fn duplicate_logical_operand(span: &Span, operand_text: &str) -> LisetteDiagnostic {
288    LisetteDiagnostic::warn("Duplicate logical operand")
289        .with_lint_code("duplicate_logical_operand")
290        .with_span_label(span, "accidental repetition")
291        .with_help(format!("Simplify to `{operand_text}`"))
292}
293
294pub fn bool_literal_comparison(span: &Span, replacement: &str) -> LisetteDiagnostic {
295    LisetteDiagnostic::warn("Redundant comparison to boolean literal")
296        .with_lint_code("bool_literal_comparison")
297        .with_span_label(span, "can be simpler")
298        .with_help(format!("Simplify to `{replacement}`"))
299}
300
301pub fn loop_runs_once(span: &Span) -> LisetteDiagnostic {
302    LisetteDiagnostic::warn("Loop runs at most once")
303        .with_lint_code("loop_runs_once")
304        .with_span_label(span, "the body always exits before looping back")
305        .with_help(
306            "The body always exits on the first iteration, so the loop never repeats. Make the exit conditional, or remove the loop.",
307        )
308}
309
310pub fn needless_return(span: &Span) -> LisetteDiagnostic {
311    LisetteDiagnostic::warn("Needless `return`")
312        .with_lint_code("needless_return")
313        .with_span_label(span, "redundant in tail position")
314        .with_help("The final expression of a function is its return value. Drop `return` and keep the value")
315}
316
317pub fn identical_if_branches(span: &Span) -> LisetteDiagnostic {
318    LisetteDiagnostic::warn("Identical if-else branches")
319        .with_lint_code("identical_if_branches")
320        .with_span_label(span, "both branches are equivalent")
321        .with_help("Remove the `if` and keep a single copy of the branch body")
322}
323
324pub fn needless_bool(span: &Span, consequence_is_true: bool) -> LisetteDiagnostic {
325    let help = if consequence_is_true {
326        "Replace this `if... else` with the condition itself"
327    } else {
328        "Replace this `if... else` with the negated condition"
329    };
330
331    LisetteDiagnostic::warn("Needless boolean if-else")
332        .with_lint_code("needless_bool")
333        .with_span_label(span, "can be simpler")
334        .with_help(help)
335}
336
337pub fn redundant_pattern_matching(span: &Span, predicate: &str) -> LisetteDiagnostic {
338    LisetteDiagnostic::warn("Redundant pattern matching")
339        .with_lint_code("redundant_pattern_matching")
340        .with_span_label(span, "can be simpler")
341        .with_help(format!("Replace this `match` with `.{predicate}()`"))
342}
343
344pub fn empty_match_arm(span: &Span) -> LisetteDiagnostic {
345    LisetteDiagnostic::warn("Empty match arm")
346        .with_lint_code("empty_match_arm")
347        .with_span_label(span, "forgotten stub?")
348        .with_help("Return `()` to indicate an intentional no-op in a match arm")
349}
350
351pub fn unnecessary_parens(span: &Span, keyword: &str) -> LisetteDiagnostic {
352    LisetteDiagnostic::warn("Unnecessary parens")
353        .with_lint_code("excess_parens_on_condition")
354        .with_span_label(span, "remove parens")
355        .with_help(format!(
356            "Lisette does not require parens around `{}` conditions",
357            keyword
358        ))
359}
360
361pub fn match_on_literal(span: &Span) -> LisetteDiagnostic {
362    LisetteDiagnostic::warn("Ineffective match")
363        .with_lint_code("match_on_literal")
364        .with_span_label(span, "already known")
365        .with_help(
366            "Matching on a literal is ineffective, because this always succeeds. Did you mean to match on a variable?",
367        )
368}
369
370pub fn single_arm_match(span: &Span, pattern_suggestion: &str) -> LisetteDiagnostic {
371    LisetteDiagnostic::warn("Ineffective match")
372        .with_lint_code("single_arm_match")
373        .with_span_label(span, "should be `if let`")
374        .with_help(format!(
375            "A match with a single meaningful arm is ineffective. Use `if let {} = value {{ ... }}` instead.",
376            pattern_suggestion
377        ))
378}
379
380pub fn uninterpolated_fstring(span: &Span) -> LisetteDiagnostic {
381    LisetteDiagnostic::warn("Uninterpolated f-string")
382        .with_lint_code("uninterpolated_fstring")
383        .with_span_label(span, "zero interpolations")
384        .with_help("Remove the `f` prefix. A string without interpolations does not need to be a format string")
385}
386
387pub fn unnecessary_raw_string(span: &Span) -> LisetteDiagnostic {
388    LisetteDiagnostic::warn("Unnecessary raw string")
389        .with_lint_code("unnecessary_raw_string")
390        .with_span_label(span, "no backslashes")
391        .with_help("Remove the `r` prefix. A string without backslashes does not need to be raw")
392}
393
394pub fn invisible_in_string(
395    span: &Span,
396    codepoint: u32,
397    name: &str,
398    is_bidi: bool,
399) -> LisetteDiagnostic {
400    let (title, code, help) = if is_bidi {
401        (
402            "Bidirectional character in string",
403            "bidi_in_string",
404            "Bidirectional control characters can reorder surrounding text and enable source-spoofing attacks. If intentional, write it as a `\\u` escape so it is visible in source; otherwise remove it.",
405        )
406    } else {
407        (
408            "Invisible character in string",
409            "invisible_in_string",
410            "Invisible characters in strings can hide bugs and silently shift meaning. Remove the character, or replace it with the visible character you meant.",
411        )
412    };
413    LisetteDiagnostic::warn(title)
414        .with_lint_code(code)
415        .with_span_label(span, format!("contains U+{codepoint:04X} ({name})"))
416        .with_help(help)
417}
418
419pub fn expression_only_fstring(span: &Span) -> LisetteDiagnostic {
420    LisetteDiagnostic::warn("Expression-only f-string")
421        .with_lint_code("expression_only_fstring")
422        .with_span_label(span, "the entire f-string is an expression")
423        .with_help("Use the expression directly. Wrapping it in an f-string adds no value")
424}
425
426pub fn rest_only_slice_pattern(span: &Span, help: impl Into<String>) -> LisetteDiagnostic {
427    LisetteDiagnostic::warn("Ineffective pattern")
428        .with_lint_code("rest_only_slice_pattern")
429        .with_span_label(span, "always matches")
430        .with_help(help)
431}
432
433pub fn miscased_pascal(span: &Span, code: &str, suggested_name: &str) -> LisetteDiagnostic {
434    LisetteDiagnostic::warn("Miscased name")
435        .with_lint_code(code)
436        .with_span_label(span, "expected PascalCase")
437        .with_help(format!("Rename to `{}`", suggested_name))
438}
439
440pub fn miscased_snake(span: &Span, code: &str, suggested_name: &str) -> LisetteDiagnostic {
441    LisetteDiagnostic::warn("Miscased name")
442        .with_lint_code(code)
443        .with_span_label(span, "expected snake_case")
444        .with_help(format!("Rename to `{}`", suggested_name))
445}
446
447pub fn miscased_screaming_snake(span: &Span, suggested_name: &str) -> LisetteDiagnostic {
448    LisetteDiagnostic::error("Miscased name")
449        .with_infer_code("constant_not_screaming_snake_case")
450        .with_span_label(span, "expected SCREAMING_SNAKE_CASE")
451        .with_help(format!("Rename to `{}`", suggested_name))
452}
453
454pub fn unused_field(span: &Span) -> LisetteDiagnostic {
455    LisetteDiagnostic::warn("Unused field")
456        .with_lint_code("unused_struct_field")
457        .with_span_label(span, "never read")
458        .with_help("Use or remove this field")
459}
460
461pub fn unused_variant(span: &Span) -> LisetteDiagnostic {
462    LisetteDiagnostic::warn("Unused variant")
463        .with_lint_code("unused_enum_variant")
464        .with_span_label(span, "never constructed or matched")
465        .with_help("Use or remove this enum variant")
466}
467
468pub fn unused_import(span: &Span) -> LisetteDiagnostic {
469    LisetteDiagnostic::warn("Unused import")
470        .with_lint_code("unused_import")
471        .with_span_label(span, "never used")
472        .with_help("Use or remove this import")
473}
474
475pub fn unused_type(span: &Span) -> LisetteDiagnostic {
476    LisetteDiagnostic::warn("Unused type")
477        .with_lint_code("unused_type")
478        .with_span_label(span, "never used")
479        .with_help("Use or remove this type")
480}
481
482pub fn unused_function(span: &Span) -> LisetteDiagnostic {
483    LisetteDiagnostic::warn("Unused function")
484        .with_lint_code("unused_function")
485        .with_span_label(span, "never called")
486        .with_help("Call or remove this function")
487}
488
489pub fn unused_constant(span: &Span) -> LisetteDiagnostic {
490    LisetteDiagnostic::warn("Unused constant")
491        .with_lint_code("unused_constant")
492        .with_span_label(span, "never used")
493        .with_help("Use or remove this constant")
494}
495
496pub fn private_type_in_public_api(
497    span: Option<&Span>,
498    private_type: &str,
499    public_definition: &str,
500) -> LisetteDiagnostic {
501    let mut diagnostic = LisetteDiagnostic::warn(format!(
502        "Private type `{}` in public API",
503        private_type
504    ))
505    .with_lint_code("internal_type_leak")
506    .with_help(format!(
507        "`{}` is private but exposed by `{}`, which is public. Add `pub` to the private type or remove it from the public API",
508        private_type, public_definition
509    ));
510
511    if let Some(s) = span {
512        diagnostic = diagnostic.with_span_label(s, "private");
513    }
514
515    diagnostic
516}
517
518pub fn unknown_attribute(span: &Span, name: &str) -> LisetteDiagnostic {
519    LisetteDiagnostic::warn("Unknown attribute")
520        .with_lint_code("unknown_attribute")
521        .with_span_label(span, "not recognized")
522        .with_help(format!(
523            "`{}` is not a recognized attribute. Known attributes: `#[json]`, `#[xml]`, `#[yaml]`, `#[toml]`, `#[db]`, `#[bson]`, `#[msgpack]`, `#[mapstructure]`, `#[tag]`",
524            name
525        ))
526}
527
528pub fn tag_has_alias(span: &Span, key: &str) -> LisetteDiagnostic {
529    LisetteDiagnostic::warn("Prefer predefined tag alias")
530        .with_lint_code("tag_has_alias")
531        .with_span_label(span, "use alias instead")
532        .with_help(format!(
533            "Use `#[{}(...)]` instead of `#[tag(...)]` for better validation",
534            key
535        ))
536}
537
538pub fn unknown_tag_option(span: &Span, option: &str) -> LisetteDiagnostic {
539    LisetteDiagnostic::warn("Unknown tag option")
540        .with_lint_code("unknown_tag_option")
541        .with_span_label(span, "not recognized")
542        .with_help(format!(
543            "`{}` is not a recognized tag option. Known options: `snake_case`, `camel_case`, `omitempty`, `!omitempty`, `skip`, `string`",
544            option
545        ))
546}
547
548pub fn trim_charset_misuse(span: &Span, function: &str) -> LisetteDiagnostic {
549    LisetteDiagnostic::warn(format!("Misuse of `{function}`"))
550        .with_lint_code("trim_charset_misuse")
551        .with_span_label(span, "treated as charset")
552        .with_help(format!(
553            "`strings.{function}` removes a set of characters, not a substring. Did you mean `strings.TrimPrefix` or `strings.TrimSuffix`?"
554        ))
555}
556
557pub fn duplicate_arguments(span: &Span, module: &str, function: &str) -> LisetteDiagnostic {
558    let display_module = module.strip_prefix("go:").unwrap_or(module);
559    LisetteDiagnostic::warn("Duplicate arguments")
560        .with_lint_code("duplicate_arguments")
561        .with_span_label(span, "identical arguments")
562        .with_help(format!(
563            "Passing the same value twice to `{display_module}.{function}` makes this call a no-op. Did you mean to pass different values?"
564        ))
565}
566
567pub fn waitgroup_add_in_task(span: &Span) -> LisetteDiagnostic {
568    LisetteDiagnostic::warn("`WaitGroup.Add` inside a `task`")
569        .with_lint_code("waitgroup_add_in_task")
570        .with_span_label(span, "may run after `Wait`")
571        .with_help(
572            "If `Wait` runs before this `Add`, it sees a zero counter and returns immediately. Call `Add` before the `task`",
573        )
574}
575
576pub fn needless_range_loop(span: &Span, collection: &str) -> LisetteDiagnostic {
577    LisetteDiagnostic::warn("Needless range loop")
578        .with_lint_code("needless_range_loop")
579        .with_span_label(span, "can be simpler")
580        .with_help(format!(
581            "This loop exposes the index only to access elements of `{collection}`. Iterate directly over the elements with `for value in {collection}`"
582        ))
583}