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::info(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::info("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::info("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 negated_equality(span: &Span, is_equal: bool) -> LisetteDiagnostic {
251    let (from, to) = if is_equal {
252        ("!(a == b)", "a != b")
253    } else {
254        ("!(a != b)", "a == b")
255    };
256
257    LisetteDiagnostic::info("Negated equality comparison")
258        .with_lint_code("negated_equality")
259        .with_span_label(span, "can be simpler")
260        .with_help(format!("Rewrite `{from}` as `{to}`"))
261}
262
263pub fn tautological_comparison(span: &Span, always_true: bool) -> LisetteDiagnostic {
264    let result = if always_true { "true" } else { "false" };
265
266    LisetteDiagnostic::warn("Tautological comparison")
267        .with_lint_code("self_comparison")
268        .with_span_label(span, "comparing to itself")
269        .with_help(format!(
270            "This condition is always `{}`. Did you mean to compare different values?",
271            result
272        ))
273}
274
275pub fn unsigned_comparison(span: &Span, always_true: bool) -> LisetteDiagnostic {
276    let result = if always_true { "true" } else { "false" };
277
278    LisetteDiagnostic::warn(format!("Comparison is always {result}"))
279        .with_lint_code("unsigned_comparison")
280        .with_span_label(span, format!("always {result}"))
281        .with_help(
282            "An unsigned integer is never negative, so this comparison always has the same result. Did you mean to compare against a different value?",
283        )
284}
285
286pub fn type_limit_comparison(span: &Span, always_true: bool) -> LisetteDiagnostic {
287    let result = if always_true { "true" } else { "false" };
288
289    LisetteDiagnostic::warn(format!("Comparison is always {result}"))
290        .with_lint_code("type_limit_comparison")
291        .with_span_label(span, format!("always `{result}`"))
292        .with_help(format!(
293            "This compares against the limit of the value's type, so this comparison is always `{result}`. Did you mean to compare against a different value?"
294        ))
295}
296
297pub fn redundant_comparison(span: &Span) -> LisetteDiagnostic {
298    LisetteDiagnostic::info("Redundant comparison")
299        .with_lint_code("redundant_comparison")
300        .with_span_label(span, "redundant")
301        .with_help(
302            "This comparison is already implied by the other, so the expression is equivalent to the other side alone",
303        )
304}
305
306pub fn double_comparison(span: &Span, combined: &str) -> LisetteDiagnostic {
307    LisetteDiagnostic::info("Comparisons can be combined")
308        .with_lint_code("double_comparison")
309        .with_span_label(span, format!("simplify to `{combined}`"))
310        .with_help(format!(
311            "These two comparisons cover the same operands, so they are equivalent to a single `{combined}`."
312        ))
313}
314
315pub fn bad_bit_mask(span: &Span, always_true: bool) -> LisetteDiagnostic {
316    let (result, clause) = if always_true {
317        ("true", "always satisfy")
318    } else {
319        ("false", "unable to satisfy")
320    };
321
322    LisetteDiagnostic::warn("Incompatible bit mask")
323        .with_lint_code("bad_bit_mask")
324        .with_span_label(span, format!("always `{result}`"))
325        .with_help(format!(
326            "The mask makes this value {clause} the comparison, so it is always `{result}`. Check the mask or the constant."
327        ))
328}
329
330pub fn ineffective_bit_mask(
331    span: &Span,
332    mask_operator: &str,
333    mask: i128,
334    constant: i128,
335) -> LisetteDiagnostic {
336    LisetteDiagnostic::warn("Ineffective bit mask")
337        .with_lint_code("ineffective_bit_mask")
338        .with_span_label(span, "mask has no effect")
339        .with_help(format!(
340            "`{mask_operator} {mask}` does not change the result of comparing with `{constant}`, so the mask can be removed."
341        ))
342}
343
344pub fn equal_operands(span: &Span, note: &str) -> LisetteDiagnostic {
345    LisetteDiagnostic::warn("Equal operands")
346        .with_lint_code("equal_operands")
347        .with_span_label(span, "identical operands")
348        .with_help(format!(
349            "Both operands are identical so the result {note}. Did you mean to use different operands?"
350        ))
351}
352
353pub fn float_cmp(span: &Span, is_equal: bool) -> LisetteDiagnostic {
354    let operator = if is_equal { "==" } else { "!=" };
355
356    LisetteDiagnostic::warn("Exact float comparison")
357        .with_lint_code("float_cmp")
358        .with_span_label(span, format!("floats compared with `{operator}`"))
359        .with_help(
360            "Floating-point results are rarely bit-exact, so `==` and `!=` may not behave as intended. Compare within a tolerance instead, e.g. `math.Abs(a - b) < c`.",
361        )
362}
363
364pub fn float_equality_without_abs(span: &Span) -> LisetteDiagnostic {
365    LisetteDiagnostic::warn("Float equality without `abs`")
366        .with_lint_code("float_equality_without_abs")
367        .with_span_label(span, "difference is not wrapped in `math.Abs`")
368        .with_help(
369            "Because `a - b` is signed, this is also `true` whenever `a` is far below `b`, not only when `a` and `b` are close, so it wrongly accepts values that are nowhere near equal. Compare the magnitude instead: `math.Abs(a - b) < c`.",
370        )
371}
372
373pub fn non_negative_comparison(span: &Span, always_true: bool) -> LisetteDiagnostic {
374    let result = if always_true { "true" } else { "false" };
375
376    LisetteDiagnostic::warn(format!("Comparison is always {result}"))
377        .with_lint_code("non_negative_comparison")
378        .with_span_label(span, format!("always {result}"))
379        .with_help(
380            "A length is never negative, so this comparison always has the same result. Did you mean to compare against a different value?",
381        )
382}
383
384pub fn goos_goarch_comparison(
385    span: &Span,
386    always_true: bool,
387    const_name: &str,
388    kind: &str,
389    examples: &str,
390) -> LisetteDiagnostic {
391    let result = if always_true { "true" } else { "false" };
392
393    LisetteDiagnostic::warn(format!("Comparison is always {result}"))
394        .with_lint_code("goos_goarch_comparison")
395        .with_span_label(span, format!("always {result}"))
396        .with_help(format!(
397            "`runtime.{const_name}` only ever holds a known {kind}, and this is not one. Did you mean a valid value such as {examples}?"
398        ))
399}
400
401pub fn redundant_operation(span: &Span, always: Option<&str>) -> LisetteDiagnostic {
402    match always {
403        Some(value) => LisetteDiagnostic::info(format!("Operation always evaluates to `{value}`"))
404            .with_lint_code("redundant_operation")
405            .with_span_label(span, format!("always `{value}`"))
406            .with_help(format!("Simplify this operation to `{value}`")),
407        None => LisetteDiagnostic::info("Operation has no effect")
408            .with_lint_code("redundant_operation")
409            .with_span_label(span, "has no effect")
410            .with_help("Simplify this operation to its other operand"),
411    }
412}
413
414pub fn unnecessary_min_or_max(span: &Span, op: &str) -> LisetteDiagnostic {
415    LisetteDiagnostic::info(format!("Unnecessary `{op}` call"))
416        .with_lint_code("unnecessary_min_or_max")
417        .with_span_label(span, "always returns the same operand")
418        .with_help(format!(
419            "This `{op}` always evaluates to one of its operands, so it has no effect. Simplify it to that operand."
420        ))
421}
422
423pub fn integer_division_to_zero(span: &Span) -> LisetteDiagnostic {
424    LisetteDiagnostic::warn("Integer division is always `0`")
425        .with_lint_code("integer_division_to_zero")
426        .with_span_label(span, "always `0`")
427        .with_help(
428            "Dividing these integer literals truncates to `0` because the numerator is smaller in magnitude than the denominator. Did you mean floating-point division?",
429        )
430}
431
432pub fn verbose_failure_propagation(span: &Span) -> LisetteDiagnostic {
433    LisetteDiagnostic::info("Verbose failure propagation")
434        .with_lint_code("verbose_failure_propagation")
435        .with_span_label(span, "verbose")
436        .with_help("Use `?` to propagate the failure concisely")
437}
438
439pub fn almost_swapped(span: &Span, first: &str, second: &str) -> LisetteDiagnostic {
440    LisetteDiagnostic::warn("Variables are not swapped")
441        .with_lint_code("almost_swapped")
442        .with_span_label(span, "does not swap the values")
443        .with_help(format!(
444            "`{first} = {second}` overwrites `{first}`, so the following `{second} = {first}` writes `{second}`'s own value back and the original `{first}` is lost. To swap them, save one value in a temporary variable first."
445        ))
446}
447
448pub fn self_assignment(span: &Span) -> LisetteDiagnostic {
449    LisetteDiagnostic::warn("Self-assignment")
450        .with_lint_code("self_assignment")
451        .with_span_label(span, "assigning to itself")
452        .with_help("Correct this assignment")
453}
454
455pub fn manual_compound_assignment(span: &Span, symbol: &str) -> LisetteDiagnostic {
456    LisetteDiagnostic::info("Manual compound assignment")
457        .with_lint_code("manual_compound_assignment")
458        .with_span_label(span, "can be simpler")
459        .with_help(format!("Use the `{symbol}` compound assignment operator"))
460}
461
462pub fn regexp_in_loop(span: &Span) -> LisetteDiagnostic {
463    LisetteDiagnostic::info("Regexp recompiled on every iteration")
464        .with_lint_code("regexp_in_loop")
465        .with_span_label(span, "compiled each time through the loop")
466        .with_help(
467            "Compile the pattern once outside the loop and reuse it: `regexp.MustCompile` for a known-valid pattern, or `regexp.Compile` to keep handling the error",
468        )
469}
470
471pub fn manual_is_empty(span: &Span, replacement: &str) -> LisetteDiagnostic {
472    LisetteDiagnostic::info("Length comparison can use `is_empty()`")
473        .with_lint_code("manual_is_empty")
474        .with_span_label(span, "can be simpler")
475        .with_help(format!("Simplify to `{replacement}`"))
476}
477
478pub fn manual_find(span: &Span, receiver: &str, predicate: &str) -> LisetteDiagnostic {
479    LisetteDiagnostic::info("Manual `find`")
480        .with_lint_code("manual_find")
481        .with_span_label(span, "can use `find`")
482        .with_help(format!(
483            "`filter(...).get(0)` builds the whole filtered slice. Use `{receiver}.find({predicate})` to return the first match directly"
484        ))
485}
486
487pub fn redundant_slice_bounds(span: &Span, replacement: &str) -> LisetteDiagnostic {
488    LisetteDiagnostic::info("Redundant slice bounds")
489        .with_lint_code("redundant_slice_bounds")
490        .with_span_label(span, "can be simpler")
491        .with_help(format!("Simplify to `{replacement}`"))
492}
493
494pub fn duplicate_logical_operand(span: &Span, operand_text: &str) -> LisetteDiagnostic {
495    LisetteDiagnostic::warn("Duplicate logical operand")
496        .with_lint_code("duplicate_logical_operand")
497        .with_span_label(span, "accidental repetition")
498        .with_help(format!("Simplify to `{operand_text}`"))
499}
500
501pub fn bool_literal_comparison(span: &Span, replacement: &str) -> LisetteDiagnostic {
502    LisetteDiagnostic::info("Redundant comparison to boolean literal")
503        .with_lint_code("bool_literal_comparison")
504        .with_span_label(span, "can be simpler")
505        .with_help(format!("Simplify to `{replacement}`"))
506}
507
508pub fn loop_runs_once(span: &Span) -> LisetteDiagnostic {
509    LisetteDiagnostic::warn("Loop runs at most once")
510        .with_lint_code("loop_runs_once")
511        .with_span_label(span, "the body always exits before looping back")
512        .with_help(
513            "The body always exits on the first iteration, so the loop never repeats. Make the exit conditional, or remove the loop.",
514        )
515}
516
517pub fn unnecessary_return(span: &Span) -> LisetteDiagnostic {
518    LisetteDiagnostic::info("Unnecessary `return`")
519        .with_lint_code("unnecessary_return")
520        .with_span_label(span, "redundant in tail position")
521        .with_help("The final expression of a function is its return value. Drop `return` and keep the value")
522}
523
524pub fn identical_if_branches(span: &Span) -> LisetteDiagnostic {
525    LisetteDiagnostic::warn("Identical if-else branches")
526        .with_lint_code("identical_if_branches")
527        .with_span_label(span, "both branches are equivalent")
528        .with_help("Remove the `if` and keep a single copy of the branch body")
529}
530
531pub fn collapsible_if(span: &Span) -> LisetteDiagnostic {
532    LisetteDiagnostic::info("Collapsible `if`")
533        .with_lint_code("collapsible_if")
534        .with_span_label(span, "can be merged into the outer `if`")
535        .with_help("Merge this nested `if` into the outer condition with `&&`")
536}
537
538pub fn redundant_else(span: &Span) -> LisetteDiagnostic {
539    LisetteDiagnostic::info("Redundant `else`")
540        .with_lint_code("redundant_else")
541        .with_span_label(span, "unnecessary")
542        .with_help(
543            "The `if` branch always exits, so the `else` only adds nesting. Drop `else` and move its body to follow the `if`",
544        )
545}
546
547pub fn identical_match_arms(span: &Span) -> LisetteDiagnostic {
548    LisetteDiagnostic::warn("Identical match arms")
549        .with_lint_code("identical_match_arms")
550        .with_span_label(span, "every arm is identical")
551        .with_help(
552            "All `match` arms resolve to the same value. Did you mean for the arms to differ?",
553        )
554}
555
556pub fn unnecessary_bool(span: &Span, consequence_is_true: bool) -> LisetteDiagnostic {
557    let help = if consequence_is_true {
558        "Replace this `if... else` with the condition itself"
559    } else {
560        "Replace this `if... else` with the negated condition"
561    };
562
563    LisetteDiagnostic::info("Unnecessary boolean if-else")
564        .with_lint_code("unnecessary_bool")
565        .with_span_label(span, "can be simpler")
566        .with_help(help)
567}
568
569pub fn redundant_pattern_matching(span: &Span, predicate: &str) -> LisetteDiagnostic {
570    LisetteDiagnostic::info("Redundant pattern matching")
571        .with_lint_code("redundant_pattern_matching")
572        .with_span_label(span, "can be simpler")
573        .with_help(format!("Replace this `match` with `.{predicate}()`"))
574}
575
576pub fn manual_map(span: &Span) -> LisetteDiagnostic {
577    LisetteDiagnostic::info("Manual map")
578        .with_lint_code("manual_map")
579        .with_span_label(span, "can be simpler")
580        .with_help("Replace this `match` with `.map(...)`")
581}
582
583pub fn manual_unwrap_or(span: &Span, lazy_default: bool) -> LisetteDiagnostic {
584    let method = if lazy_default {
585        "unwrap_or_else"
586    } else {
587        "unwrap_or"
588    };
589    LisetteDiagnostic::info("Manual `unwrap_or`")
590        .with_lint_code("manual_unwrap_or")
591        .with_span_label(span, "can be simpler")
592        .with_help(format!("Replace this `match` with `.{method}(...)`"))
593}
594
595pub fn manual_map_or(span: &Span, lazy_default: bool) -> LisetteDiagnostic {
596    let replacement = if lazy_default {
597        ".map_or_else(...)"
598    } else {
599        ".map_or(...)"
600    };
601    LisetteDiagnostic::info("Manual `map_or`")
602        .with_lint_code("manual_map_or")
603        .with_span_label(span, "can be simpler")
604        .with_help(format!("Replace this `match` with `{replacement}`"))
605}
606
607pub fn manual_filter(span: &Span) -> LisetteDiagnostic {
608    LisetteDiagnostic::info("Manual `filter`")
609        .with_lint_code("manual_filter")
610        .with_span_label(span, "can be simpler")
611        .with_help("Replace this `match` with `.filter(...)`")
612}
613
614pub fn manual_ok_or(span: &Span, lazy: bool) -> LisetteDiagnostic {
615    let method = if lazy { "ok_or_else" } else { "ok_or" };
616    LisetteDiagnostic::info("Manual `ok_or`")
617        .with_lint_code("manual_ok_or")
618        .with_span_label(span, "can be simpler")
619        .with_help(format!("Replace this `match` with `.{method}(...)`"))
620}
621
622pub fn manual_ok_err(span: &Span, method: &str) -> LisetteDiagnostic {
623    LisetteDiagnostic::info(format!("Manual `{method}`"))
624        .with_lint_code("manual_ok_err")
625        .with_span_label(span, "can be simpler")
626        .with_help(format!("Replace this `match` with `.{method}()`"))
627}
628
629pub fn needless_match(span: &Span, subject: &str) -> LisetteDiagnostic {
630    LisetteDiagnostic::info("Needless `match`")
631        .with_lint_code("needless_match")
632        .with_span_label(span, "unnecessary")
633        .with_help(format!(
634            "Every arm rebuilds the subject unchanged. Replace this `match` with `{subject}`"
635        ))
636}
637
638pub fn map_unwrap_or(span: &Span) -> LisetteDiagnostic {
639    LisetteDiagnostic::info("Manual `map_or`")
640        .with_lint_code("map_unwrap_or")
641        .with_span_label(span, "can be simpler")
642        .with_help("Replace `.map(f).unwrap_or(d)` with `.map_or(d, f)`")
643}
644
645pub fn bind_instead_of_map(span: &Span, wrapper: &str) -> LisetteDiagnostic {
646    LisetteDiagnostic::info("Manual `map`")
647        .with_lint_code("bind_instead_of_map")
648        .with_span_label(span, "can be simpler")
649        .with_help(format!(
650            "Replace `.and_then(|x| {wrapper}(y))` with `.map(|x| y)`"
651        ))
652}
653
654pub fn map_flatten(span: &Span) -> LisetteDiagnostic {
655    LisetteDiagnostic::info("Manual `and_then`")
656        .with_lint_code("map_flatten")
657        .with_span_label(span, "can be simpler")
658        .with_help("Replace `.map(f).flatten()` with `.and_then(f)`")
659}
660
661pub fn map_identity(span: &Span) -> LisetteDiagnostic {
662    LisetteDiagnostic::info("Redundant `map`")
663        .with_lint_code("map_identity")
664        .with_span_label(span, "does nothing")
665        .with_help("Remove `.map(|x| x)`, which returns its input unchanged")
666}
667
668pub fn unnecessary_map_on_constructor(
669    span: &Span,
670    variant: &str,
671    method: &str,
672) -> LisetteDiagnostic {
673    LisetteDiagnostic::info(format!("Unnecessary `{method}`"))
674        .with_lint_code("unnecessary_map_on_constructor")
675        .with_span_label(span, "can be simpler")
676        .with_help(format!(
677            "Replace `{variant}(x).{method}(f)` with `{variant}(f(x))`"
678        ))
679}
680
681pub fn map_or_none(span: &Span, replacement: &str) -> LisetteDiagnostic {
682    let help = if replacement == "ok" {
683        "Replace `.map_or(None, Some)` with `.ok()`".to_string()
684    } else {
685        format!("Replace `.map_or(None, f)` with `.{replacement}(f)`")
686    };
687    LisetteDiagnostic::info(format!("Manual `{replacement}`"))
688        .with_lint_code("map_or_none")
689        .with_span_label(span, "can be simpler")
690        .with_help(help)
691}
692
693pub fn redundant_closure(span: &Span, callee: &str) -> LisetteDiagnostic {
694    LisetteDiagnostic::info("Redundant closure")
695        .with_lint_code("redundant_closure")
696        .with_span_label(span, "can be simpler")
697        .with_help(format!("Replace this closure with `{callee}`"))
698}
699
700pub fn empty_match_arm(span: &Span) -> LisetteDiagnostic {
701    LisetteDiagnostic::warn("Empty match arm")
702        .with_lint_code("empty_match_arm")
703        .with_span_label(span, "forgotten stub?")
704        .with_help("Return `()` to indicate an intentional no-op in a match arm")
705}
706
707pub fn unnecessary_parens(span: &Span, keyword: &str) -> LisetteDiagnostic {
708    LisetteDiagnostic::info("Unnecessary parens")
709        .with_lint_code("excess_parens_on_condition")
710        .with_span_label(span, "remove parens")
711        .with_help(format!(
712            "Lisette does not require parens around `{}` conditions",
713            keyword
714        ))
715}
716
717pub fn match_on_literal(span: &Span) -> LisetteDiagnostic {
718    LisetteDiagnostic::warn("Ineffective match")
719        .with_lint_code("match_on_literal")
720        .with_span_label(span, "already known")
721        .with_help(
722            "Matching on a literal is ineffective, because this always succeeds. Did you mean to match on a variable?",
723        )
724}
725
726pub fn match_as_if_let(span: &Span, pattern_suggestion: &str) -> LisetteDiagnostic {
727    LisetteDiagnostic::info("`match` reducible to `if let`")
728        .with_lint_code("match_as_if_let")
729        .with_span_label(span, "can be simpler")
730        .with_help(format!(
731            "Replace this `match` with `if let {} = value {{ ... }}`",
732            pattern_suggestion
733        ))
734}
735
736pub fn single_arm_select(span: &Span, receive: &str) -> LisetteDiagnostic {
737    LisetteDiagnostic::info("Single-arm `select`")
738        .with_lint_code("single_arm_select")
739        .with_span_label(span, "waits on a single operation")
740        .with_help(format!(
741            "A `select` with one arm makes no choice between channel operations. Use `match {receive} {{ ... }}` directly"
742        ))
743}
744
745pub fn match_on_bool(span: &Span) -> LisetteDiagnostic {
746    LisetteDiagnostic::info("Match on boolean")
747        .with_lint_code("match_on_bool")
748        .with_span_label(span, "should be `if`")
749        .with_help("A `match` on a boolean is better written as an `if` expression")
750}
751
752pub fn match_single_binding(span: &Span, binding: &str) -> LisetteDiagnostic {
753    LisetteDiagnostic::info("Ineffective match")
754        .with_lint_code("match_single_binding")
755        .with_span_label(span, "should be `let`")
756        .with_help(format!(
757            "A match with a single binding is ineffective. Use `let {} = value` instead.",
758            binding
759        ))
760}
761
762pub fn let_and_return(span: &Span) -> LisetteDiagnostic {
763    LisetteDiagnostic::info("Redundant binding before return")
764        .with_lint_code("let_and_return")
765        .with_span_label(span, "bound and immediately returned")
766        .with_help("Return the value directly instead of binding it first")
767}
768
769pub fn uninterpolated_fstring(span: &Span) -> LisetteDiagnostic {
770    LisetteDiagnostic::info("Uninterpolated f-string")
771        .with_lint_code("uninterpolated_fstring")
772        .with_span_label(span, "zero interpolations")
773        .with_help("Remove the `f` prefix. A string without interpolations does not need to be a format string")
774}
775
776pub fn unnecessary_raw_string(span: &Span) -> LisetteDiagnostic {
777    LisetteDiagnostic::info("Unnecessary raw string")
778        .with_lint_code("unnecessary_raw_string")
779        .with_span_label(span, "no backslashes")
780        .with_help("Remove the `r` prefix. A string without backslashes does not need to be raw")
781}
782
783pub fn invisible_in_string(
784    span: &Span,
785    codepoint: u32,
786    name: &str,
787    is_bidi: bool,
788) -> LisetteDiagnostic {
789    let (title, code, help) = if is_bidi {
790        (
791            "Bidirectional character in string",
792            "bidi_in_string",
793            "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.",
794        )
795    } else {
796        (
797            "Invisible character in string",
798            "invisible_in_string",
799            "Invisible characters in strings can hide bugs and silently shift meaning. Remove the character, or replace it with the visible character you meant.",
800        )
801    };
802    LisetteDiagnostic::warn(title)
803        .with_lint_code(code)
804        .with_span_label(span, format!("contains U+{codepoint:04X} ({name})"))
805        .with_help(help)
806}
807
808pub fn expression_only_fstring(span: &Span) -> LisetteDiagnostic {
809    LisetteDiagnostic::info("Expression-only f-string")
810        .with_lint_code("expression_only_fstring")
811        .with_span_label(span, "the entire f-string is an expression")
812        .with_help("Use the expression directly. Wrapping it in an f-string adds no value")
813}
814
815pub fn rest_only_slice_pattern(span: &Span, help: impl Into<String>) -> LisetteDiagnostic {
816    LisetteDiagnostic::info("Ineffective pattern")
817        .with_lint_code("rest_only_slice_pattern")
818        .with_span_label(span, "always matches")
819        .with_help(help)
820}
821
822pub fn miscased_pascal(span: &Span, code: &str, suggested_name: &str) -> LisetteDiagnostic {
823    LisetteDiagnostic::warn("Miscased name")
824        .with_lint_code(code)
825        .with_span_label(span, "expected PascalCase")
826        .with_help(format!("Rename to `{}`", suggested_name))
827}
828
829pub fn miscased_snake(span: &Span, code: &str, suggested_name: &str) -> LisetteDiagnostic {
830    LisetteDiagnostic::warn("Miscased name")
831        .with_lint_code(code)
832        .with_span_label(span, "expected snake_case")
833        .with_help(format!("Rename to `{}`", suggested_name))
834}
835
836pub fn miscased_screaming_snake(span: &Span, suggested_name: &str) -> LisetteDiagnostic {
837    LisetteDiagnostic::error("Miscased name")
838        .with_infer_code("constant_not_screaming_snake_case")
839        .with_span_label(span, "expected SCREAMING_SNAKE_CASE")
840        .with_help(format!("Rename to `{}`", suggested_name))
841}
842
843pub fn unused_field(span: &Span) -> LisetteDiagnostic {
844    LisetteDiagnostic::warn("Unused field")
845        .with_lint_code("unused_struct_field")
846        .with_span_label(span, "never read")
847        .with_help("Use or remove this field")
848}
849
850pub fn unused_variant(span: &Span) -> LisetteDiagnostic {
851    LisetteDiagnostic::warn("Unused variant")
852        .with_lint_code("unused_enum_variant")
853        .with_span_label(span, "never constructed or matched")
854        .with_help("Use or remove this enum variant")
855}
856
857pub fn unused_import(span: &Span) -> LisetteDiagnostic {
858    LisetteDiagnostic::warn("Unused import")
859        .with_lint_code("unused_import")
860        .with_span_label(span, "never used")
861        .with_help("Use or remove this import")
862}
863
864pub fn unused_type(span: &Span) -> LisetteDiagnostic {
865    LisetteDiagnostic::warn("Unused type")
866        .with_lint_code("unused_type")
867        .with_span_label(span, "never used")
868        .with_help("Use or remove this type")
869}
870
871pub fn unused_function(span: &Span) -> LisetteDiagnostic {
872    LisetteDiagnostic::warn("Unused function")
873        .with_lint_code("unused_function")
874        .with_span_label(span, "never called")
875        .with_help("Call or remove this function")
876}
877
878pub fn unused_constant(span: &Span) -> LisetteDiagnostic {
879    LisetteDiagnostic::warn("Unused constant")
880        .with_lint_code("unused_constant")
881        .with_span_label(span, "never used")
882        .with_help("Use or remove this constant")
883}
884
885pub fn private_type_in_public_api(
886    span: Option<&Span>,
887    private_type: &str,
888    public_definition: &str,
889) -> LisetteDiagnostic {
890    let mut diagnostic = LisetteDiagnostic::warn(format!(
891        "Private type `{}` in public API",
892        private_type
893    ))
894    .with_lint_code("internal_type_leak")
895    .with_help(format!(
896        "`{}` is private but exposed by `{}`, which is public. Add `pub` to the private type or remove it from the public API",
897        private_type, public_definition
898    ));
899
900    if let Some(s) = span {
901        diagnostic = diagnostic.with_span_label(s, "private");
902    }
903
904    diagnostic
905}
906
907pub fn unknown_attribute(span: &Span, name: &str, known: &[&str]) -> LisetteDiagnostic {
908    let known_list = known
909        .iter()
910        .map(|attribute| format!("`#[{attribute}]`"))
911        .collect::<Vec<_>>()
912        .join(", ");
913    LisetteDiagnostic::warn("Unknown attribute")
914        .with_lint_code("unknown_attribute")
915        .with_span_label(span, "not recognized")
916        .with_help(format!(
917            "`{name}` is not a recognized attribute. Known attributes: {known_list}"
918        ))
919}
920
921pub fn tag_has_alias(span: &Span, key: &str) -> LisetteDiagnostic {
922    LisetteDiagnostic::info("Prefer predefined tag alias")
923        .with_lint_code("tag_has_alias")
924        .with_span_label(span, "use alias instead")
925        .with_help(format!(
926            "Use `#[{}(...)]` instead of `#[tag(...)]` for better validation",
927            key
928        ))
929}
930
931pub fn unknown_tag_option(span: &Span, option: &str) -> LisetteDiagnostic {
932    LisetteDiagnostic::warn("Unknown tag option")
933        .with_lint_code("unknown_tag_option")
934        .with_span_label(span, "not recognized")
935        .with_help(format!(
936            "`{}` is not a recognized tag option. Known options: `snake_case`, `camel_case`, `omitempty`, `!omitempty`, `skip`, `string`",
937            option
938        ))
939}
940
941pub fn trim_charset_misuse(span: &Span, function: &str) -> LisetteDiagnostic {
942    LisetteDiagnostic::warn(format!("Misuse of `{function}`"))
943        .with_lint_code("trim_charset_misuse")
944        .with_span_label(span, "treated as charset")
945        .with_help(format!(
946            "`strings.{function}` removes a set of characters, not a substring. Did you mean `strings.TrimPrefix` or `strings.TrimSuffix`?"
947        ))
948}
949
950pub fn duplicate_arguments(span: &Span, module: &str, function: &str) -> LisetteDiagnostic {
951    let display_module = module.strip_prefix("go:").unwrap_or(module);
952    LisetteDiagnostic::warn("Duplicate arguments")
953        .with_lint_code("duplicate_arguments")
954        .with_span_label(span, "identical arguments")
955        .with_help(format!(
956            "Passing the same value twice to `{display_module}.{function}` makes this call a no-op. Did you mean to pass different values?"
957        ))
958}
959
960pub fn manual_equal_fold(
961    span: &Span,
962    negated: bool,
963    namespace: &str,
964    left_arg: &str,
965    right_arg: &str,
966) -> LisetteDiagnostic {
967    let prefix = if negated { "!" } else { "" };
968    LisetteDiagnostic::info("Inefficient comparison")
969        .with_lint_code("manual_equal_fold")
970        .with_span_label(span, "can use `strings.EqualFold`")
971        .with_help(format!(
972            "Use `{prefix}{namespace}.EqualFold({left_arg}, {right_arg})` to compare case-insensitively in one call"
973        ))
974}
975
976pub fn manual_bytes_equal(
977    span: &Span,
978    negated: bool,
979    namespace: &str,
980    left_arg: &str,
981    right_arg: &str,
982) -> LisetteDiagnostic {
983    let prefix = if negated { "!" } else { "" };
984    LisetteDiagnostic::info("Manual `bytes.Equal`")
985        .with_lint_code("manual_bytes_equal")
986        .with_span_label(span, "can use `bytes.Equal`")
987        .with_help(format!(
988            "Use `{prefix}{namespace}.Equal({left_arg}, {right_arg})` to compare byte slices directly"
989        ))
990}
991
992pub fn redundant_sprintf(span: &Span, namespace: &str, value: &str) -> LisetteDiagnostic {
993    LisetteDiagnostic::info("Redundant `Sprintf`")
994        .with_lint_code("redundant_sprintf")
995        .with_span_label(span, "returns its argument unchanged")
996        .with_help(format!(
997            "`{namespace}.Sprintf(\"%s\", {value})` formats a string as itself. Use `{value}` directly"
998        ))
999}
1000
1001pub fn manual_replace_all(
1002    span: &Span,
1003    namespace: &str,
1004    s: &str,
1005    old: &str,
1006    new: &str,
1007) -> LisetteDiagnostic {
1008    LisetteDiagnostic::info("Manual `strings.ReplaceAll`")
1009        .with_lint_code("manual_replace_all")
1010        .with_span_label(span, "can use `strings.ReplaceAll`")
1011        .with_help(format!(
1012            "`{namespace}.Replace({s}, {old}, {new}, -1)` replaces every occurrence. Use `{namespace}.ReplaceAll({s}, {old}, {new})`"
1013        ))
1014}
1015
1016pub fn manual_time_since(span: &Span, namespace: &str, arg: &str) -> LisetteDiagnostic {
1017    LisetteDiagnostic::info("Manual `time.Since`")
1018        .with_lint_code("manual_time_since")
1019        .with_span_label(span, "can use `time.Since`")
1020        .with_help(format!(
1021            "`{namespace}.Since({arg})` is shorthand for `{namespace}.Now().Sub({arg})`"
1022        ))
1023}
1024
1025pub fn manual_time_until(span: &Span, namespace: &str, receiver: &str) -> LisetteDiagnostic {
1026    LisetteDiagnostic::info("Manual `time.Until`")
1027        .with_lint_code("manual_time_until")
1028        .with_span_label(span, "can use `time.Until`")
1029        .with_help(format!(
1030            "`{namespace}.Until({receiver})` is shorthand for `{receiver}.Sub({namespace}.Now())`"
1031        ))
1032}
1033
1034pub fn lost_query_mutation(span: &Span, method: &str) -> LisetteDiagnostic {
1035    LisetteDiagnostic::warn("Lost query mutation")
1036        .with_lint_code("lost_query_mutation")
1037        .with_span_label(span, "mutates a discarded copy")
1038        .with_help(format!(
1039            "`URL.Query` returns a fresh copy, so this `{method}` has no effect. Bind the copy returned by `Query()` to an identifier, mutate it, then assign `values.Encode()` back to the URL's `RawQuery` field."
1040        ))
1041}
1042
1043pub fn waitgroup_add_in_task(span: &Span) -> LisetteDiagnostic {
1044    LisetteDiagnostic::warn("`WaitGroup.Add` inside a `task`")
1045        .with_lint_code("waitgroup_add_in_task")
1046        .with_span_label(span, "may run after `Wait`")
1047        .with_help(
1048            "Prefer `wg.Go(|| ...)`, which counts the task and starts it in one step and runs `Done` for you, or move `Add` before the `task`",
1049        )
1050}
1051
1052pub fn deprecated_api(span: &Span, message: &str) -> LisetteDiagnostic {
1053    LisetteDiagnostic::warn("Use of deprecated API")
1054        .with_lint_code("deprecated")
1055        .with_span_label(span, "deprecated")
1056        .with_help(message)
1057}
1058
1059pub fn lost_cancel(span: &Span) -> LisetteDiagnostic {
1060    LisetteDiagnostic::warn("Context leaking")
1061        .with_lint_code("lost_cancel")
1062        .with_span_label(span, "never called")
1063        .with_help(
1064            "Call this cancel function (usually `defer cancel()`) to release the context, or it leaks until the parent is canceled",
1065        )
1066}
1067
1068pub fn exit_after_defer(span: &Span) -> LisetteDiagnostic {
1069    LisetteDiagnostic::warn("`os.Exit` skips `defer`")
1070        .with_lint_code("exit_after_defer")
1071        .with_span_label(span, "exits before the `defer` above can run")
1072        .with_help(
1073            "`os.Exit` will terminate the process without running deferred calls. Run the cleanup before exiting instead of deferring it",
1074        )
1075}
1076
1077pub fn unnecessary_range_loop(span: &Span, collection: &str) -> LisetteDiagnostic {
1078    LisetteDiagnostic::info("Unnecessary range loop")
1079        .with_lint_code("unnecessary_range_loop")
1080        .with_span_label(span, "can be simpler")
1081        .with_help(format!(
1082            "This loop exposes the index only to access elements of `{collection}`. Iterate directly over the elements with `for value in {collection}`"
1083        ))
1084}
1085
1086pub fn out_of_domain_value(
1087    span: &Span,
1088    type_display: &str,
1089    valid_display: &str,
1090) -> LisetteDiagnostic {
1091    LisetteDiagnostic::warn("Out-of-domain value")
1092        .with_lint_code("out_of_domain_value")
1093        .with_span_label(span, "out of domain")
1094        .with_help(format!(
1095            "`{type_display}` has a closed domain (`{valid_display}`) that excludes this value"
1096        ))
1097}