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 non_negative_comparison(span: &Span, always_true: bool) -> LisetteDiagnostic {
298    let result = if always_true { "true" } else { "false" };
299
300    LisetteDiagnostic::warn(format!("Comparison is always {result}"))
301        .with_lint_code("non_negative_comparison")
302        .with_span_label(span, format!("always {result}"))
303        .with_help(
304            "A length is never negative, so this comparison always has the same result. Did you mean to compare against a different value?",
305        )
306}
307
308pub fn goos_goarch_comparison(
309    span: &Span,
310    always_true: bool,
311    const_name: &str,
312    kind: &str,
313    examples: &str,
314) -> LisetteDiagnostic {
315    let result = if always_true { "true" } else { "false" };
316
317    LisetteDiagnostic::warn(format!("Comparison is always {result}"))
318        .with_lint_code("goos_goarch_comparison")
319        .with_span_label(span, format!("always {result}"))
320        .with_help(format!(
321            "`runtime.{const_name}` only ever holds a known {kind}, and this is not one. Did you mean a valid value such as {examples}?"
322        ))
323}
324
325pub fn redundant_operation(span: &Span, always: Option<&str>) -> LisetteDiagnostic {
326    match always {
327        Some(value) => LisetteDiagnostic::info(format!("Operation always evaluates to `{value}`"))
328            .with_lint_code("redundant_operation")
329            .with_span_label(span, format!("always `{value}`"))
330            .with_help(format!("Simplify this operation to `{value}`")),
331        None => LisetteDiagnostic::info("Operation has no effect")
332            .with_lint_code("redundant_operation")
333            .with_span_label(span, "has no effect")
334            .with_help("Simplify this operation to its other operand"),
335    }
336}
337
338pub fn integer_division_to_zero(span: &Span) -> LisetteDiagnostic {
339    LisetteDiagnostic::warn("Integer division is always `0`")
340        .with_lint_code("integer_division_to_zero")
341        .with_span_label(span, "always `0`")
342        .with_help(
343            "Dividing these integer literals truncates to `0` because the numerator is smaller in magnitude than the denominator. Did you mean floating-point division?",
344        )
345}
346
347pub fn verbose_failure_propagation(span: &Span) -> LisetteDiagnostic {
348    LisetteDiagnostic::info("Verbose failure propagation")
349        .with_lint_code("verbose_failure_propagation")
350        .with_span_label(span, "verbose")
351        .with_help("Use `?` to propagate the failure concisely")
352}
353
354pub fn self_assignment(span: &Span) -> LisetteDiagnostic {
355    LisetteDiagnostic::warn("Self-assignment")
356        .with_lint_code("self_assignment")
357        .with_span_label(span, "assigning to itself")
358        .with_help("Correct this assignment")
359}
360
361pub fn manual_compound_assignment(span: &Span, symbol: &str) -> LisetteDiagnostic {
362    LisetteDiagnostic::info("Manual compound assignment")
363        .with_lint_code("manual_compound_assignment")
364        .with_span_label(span, "can be simpler")
365        .with_help(format!("Use the `{symbol}` compound assignment operator"))
366}
367
368pub fn manual_is_empty(span: &Span, replacement: &str) -> LisetteDiagnostic {
369    LisetteDiagnostic::info("Length comparison can use `is_empty()`")
370        .with_lint_code("manual_is_empty")
371        .with_span_label(span, "can be simpler")
372        .with_help(format!("Simplify to `{replacement}`"))
373}
374
375pub fn manual_find(span: &Span, receiver: &str, predicate: &str) -> LisetteDiagnostic {
376    LisetteDiagnostic::info("Manual `find`")
377        .with_lint_code("manual_find")
378        .with_span_label(span, "can use `find`")
379        .with_help(format!(
380            "`filter(...).get(0)` builds the whole filtered slice. Use `{receiver}.find({predicate})` to return the first match directly"
381        ))
382}
383
384pub fn redundant_slice_bounds(span: &Span, replacement: &str) -> LisetteDiagnostic {
385    LisetteDiagnostic::info("Redundant slice bounds")
386        .with_lint_code("redundant_slice_bounds")
387        .with_span_label(span, "can be simpler")
388        .with_help(format!("Simplify to `{replacement}`"))
389}
390
391pub fn duplicate_logical_operand(span: &Span, operand_text: &str) -> LisetteDiagnostic {
392    LisetteDiagnostic::warn("Duplicate logical operand")
393        .with_lint_code("duplicate_logical_operand")
394        .with_span_label(span, "accidental repetition")
395        .with_help(format!("Simplify to `{operand_text}`"))
396}
397
398pub fn bool_literal_comparison(span: &Span, replacement: &str) -> LisetteDiagnostic {
399    LisetteDiagnostic::info("Redundant comparison to boolean literal")
400        .with_lint_code("bool_literal_comparison")
401        .with_span_label(span, "can be simpler")
402        .with_help(format!("Simplify to `{replacement}`"))
403}
404
405pub fn loop_runs_once(span: &Span) -> LisetteDiagnostic {
406    LisetteDiagnostic::warn("Loop runs at most once")
407        .with_lint_code("loop_runs_once")
408        .with_span_label(span, "the body always exits before looping back")
409        .with_help(
410            "The body always exits on the first iteration, so the loop never repeats. Make the exit conditional, or remove the loop.",
411        )
412}
413
414pub fn unnecessary_return(span: &Span) -> LisetteDiagnostic {
415    LisetteDiagnostic::info("Unnecessary `return`")
416        .with_lint_code("unnecessary_return")
417        .with_span_label(span, "redundant in tail position")
418        .with_help("The final expression of a function is its return value. Drop `return` and keep the value")
419}
420
421pub fn identical_if_branches(span: &Span) -> LisetteDiagnostic {
422    LisetteDiagnostic::warn("Identical if-else branches")
423        .with_lint_code("identical_if_branches")
424        .with_span_label(span, "both branches are equivalent")
425        .with_help("Remove the `if` and keep a single copy of the branch body")
426}
427
428pub fn collapsible_if(span: &Span) -> LisetteDiagnostic {
429    LisetteDiagnostic::info("Collapsible `if`")
430        .with_lint_code("collapsible_if")
431        .with_span_label(span, "can be merged into the outer `if`")
432        .with_help("Merge this nested `if` into the outer condition with `&&`")
433}
434
435pub fn redundant_else(span: &Span) -> LisetteDiagnostic {
436    LisetteDiagnostic::info("Redundant `else`")
437        .with_lint_code("redundant_else")
438        .with_span_label(span, "unnecessary")
439        .with_help(
440            "The `if` branch always exits, so the `else` only adds nesting. Drop `else` and move its body to follow the `if`",
441        )
442}
443
444pub fn identical_match_arms(span: &Span) -> LisetteDiagnostic {
445    LisetteDiagnostic::warn("Identical match arms")
446        .with_lint_code("identical_match_arms")
447        .with_span_label(span, "every arm is identical")
448        .with_help(
449            "All `match` arms resolve to the same value. Did you mean for the arms to differ?",
450        )
451}
452
453pub fn unnecessary_bool(span: &Span, consequence_is_true: bool) -> LisetteDiagnostic {
454    let help = if consequence_is_true {
455        "Replace this `if... else` with the condition itself"
456    } else {
457        "Replace this `if... else` with the negated condition"
458    };
459
460    LisetteDiagnostic::info("Unnecessary boolean if-else")
461        .with_lint_code("unnecessary_bool")
462        .with_span_label(span, "can be simpler")
463        .with_help(help)
464}
465
466pub fn redundant_pattern_matching(span: &Span, predicate: &str) -> LisetteDiagnostic {
467    LisetteDiagnostic::info("Redundant pattern matching")
468        .with_lint_code("redundant_pattern_matching")
469        .with_span_label(span, "can be simpler")
470        .with_help(format!("Replace this `match` with `.{predicate}()`"))
471}
472
473pub fn manual_map(span: &Span) -> LisetteDiagnostic {
474    LisetteDiagnostic::info("Manual map")
475        .with_lint_code("manual_map")
476        .with_span_label(span, "can be simpler")
477        .with_help("Replace this `match` with `.map(...)`")
478}
479
480pub fn manual_unwrap_or(span: &Span, lazy_default: bool) -> LisetteDiagnostic {
481    let method = if lazy_default {
482        "unwrap_or_else"
483    } else {
484        "unwrap_or"
485    };
486    LisetteDiagnostic::info("Manual `unwrap_or`")
487        .with_lint_code("manual_unwrap_or")
488        .with_span_label(span, "can be simpler")
489        .with_help(format!("Replace this `match` with `.{method}(...)`"))
490}
491
492pub fn manual_map_or(span: &Span, lazy_default: bool) -> LisetteDiagnostic {
493    let replacement = if lazy_default {
494        ".map_or_else(...)"
495    } else {
496        ".map_or(...)"
497    };
498    LisetteDiagnostic::info("Manual `map_or`")
499        .with_lint_code("manual_map_or")
500        .with_span_label(span, "can be simpler")
501        .with_help(format!("Replace this `match` with `{replacement}`"))
502}
503
504pub fn redundant_closure(span: &Span, callee: &str) -> LisetteDiagnostic {
505    LisetteDiagnostic::info("Redundant closure")
506        .with_lint_code("redundant_closure")
507        .with_span_label(span, "can be simpler")
508        .with_help(format!("Replace this closure with `{callee}`"))
509}
510
511pub fn empty_match_arm(span: &Span) -> LisetteDiagnostic {
512    LisetteDiagnostic::warn("Empty match arm")
513        .with_lint_code("empty_match_arm")
514        .with_span_label(span, "forgotten stub?")
515        .with_help("Return `()` to indicate an intentional no-op in a match arm")
516}
517
518pub fn unnecessary_parens(span: &Span, keyword: &str) -> LisetteDiagnostic {
519    LisetteDiagnostic::info("Unnecessary parens")
520        .with_lint_code("excess_parens_on_condition")
521        .with_span_label(span, "remove parens")
522        .with_help(format!(
523            "Lisette does not require parens around `{}` conditions",
524            keyword
525        ))
526}
527
528pub fn match_on_literal(span: &Span) -> LisetteDiagnostic {
529    LisetteDiagnostic::warn("Ineffective match")
530        .with_lint_code("match_on_literal")
531        .with_span_label(span, "already known")
532        .with_help(
533            "Matching on a literal is ineffective, because this always succeeds. Did you mean to match on a variable?",
534        )
535}
536
537pub fn match_as_if_let(span: &Span, pattern_suggestion: &str) -> LisetteDiagnostic {
538    LisetteDiagnostic::info("`match` reducible to `if let`")
539        .with_lint_code("match_as_if_let")
540        .with_span_label(span, "can be simpler")
541        .with_help(format!(
542            "Replace this `match` with `if let {} = value {{ ... }}`",
543            pattern_suggestion
544        ))
545}
546
547pub fn single_arm_select(span: &Span, receive: &str) -> LisetteDiagnostic {
548    LisetteDiagnostic::info("Single-arm `select`")
549        .with_lint_code("single_arm_select")
550        .with_span_label(span, "waits on a single operation")
551        .with_help(format!(
552            "A `select` with one arm makes no choice between channel operations. Use `match {receive} {{ ... }}` directly"
553        ))
554}
555
556pub fn match_on_bool(span: &Span) -> LisetteDiagnostic {
557    LisetteDiagnostic::info("Match on boolean")
558        .with_lint_code("match_on_bool")
559        .with_span_label(span, "should be `if`")
560        .with_help("A `match` on a boolean is better written as an `if` expression")
561}
562
563pub fn match_single_binding(span: &Span, binding: &str) -> LisetteDiagnostic {
564    LisetteDiagnostic::info("Ineffective match")
565        .with_lint_code("match_single_binding")
566        .with_span_label(span, "should be `let`")
567        .with_help(format!(
568            "A match with a single binding is ineffective. Use `let {} = value` instead.",
569            binding
570        ))
571}
572
573pub fn let_and_return(span: &Span) -> LisetteDiagnostic {
574    LisetteDiagnostic::info("Redundant binding before return")
575        .with_lint_code("let_and_return")
576        .with_span_label(span, "bound and immediately returned")
577        .with_help("Return the value directly instead of binding it first")
578}
579
580pub fn uninterpolated_fstring(span: &Span) -> LisetteDiagnostic {
581    LisetteDiagnostic::info("Uninterpolated f-string")
582        .with_lint_code("uninterpolated_fstring")
583        .with_span_label(span, "zero interpolations")
584        .with_help("Remove the `f` prefix. A string without interpolations does not need to be a format string")
585}
586
587pub fn unnecessary_raw_string(span: &Span) -> LisetteDiagnostic {
588    LisetteDiagnostic::info("Unnecessary raw string")
589        .with_lint_code("unnecessary_raw_string")
590        .with_span_label(span, "no backslashes")
591        .with_help("Remove the `r` prefix. A string without backslashes does not need to be raw")
592}
593
594pub fn invisible_in_string(
595    span: &Span,
596    codepoint: u32,
597    name: &str,
598    is_bidi: bool,
599) -> LisetteDiagnostic {
600    let (title, code, help) = if is_bidi {
601        (
602            "Bidirectional character in string",
603            "bidi_in_string",
604            "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.",
605        )
606    } else {
607        (
608            "Invisible character in string",
609            "invisible_in_string",
610            "Invisible characters in strings can hide bugs and silently shift meaning. Remove the character, or replace it with the visible character you meant.",
611        )
612    };
613    LisetteDiagnostic::warn(title)
614        .with_lint_code(code)
615        .with_span_label(span, format!("contains U+{codepoint:04X} ({name})"))
616        .with_help(help)
617}
618
619pub fn expression_only_fstring(span: &Span) -> LisetteDiagnostic {
620    LisetteDiagnostic::info("Expression-only f-string")
621        .with_lint_code("expression_only_fstring")
622        .with_span_label(span, "the entire f-string is an expression")
623        .with_help("Use the expression directly. Wrapping it in an f-string adds no value")
624}
625
626pub fn rest_only_slice_pattern(span: &Span, help: impl Into<String>) -> LisetteDiagnostic {
627    LisetteDiagnostic::info("Ineffective pattern")
628        .with_lint_code("rest_only_slice_pattern")
629        .with_span_label(span, "always matches")
630        .with_help(help)
631}
632
633pub fn miscased_pascal(span: &Span, code: &str, suggested_name: &str) -> LisetteDiagnostic {
634    LisetteDiagnostic::warn("Miscased name")
635        .with_lint_code(code)
636        .with_span_label(span, "expected PascalCase")
637        .with_help(format!("Rename to `{}`", suggested_name))
638}
639
640pub fn miscased_snake(span: &Span, code: &str, suggested_name: &str) -> LisetteDiagnostic {
641    LisetteDiagnostic::warn("Miscased name")
642        .with_lint_code(code)
643        .with_span_label(span, "expected snake_case")
644        .with_help(format!("Rename to `{}`", suggested_name))
645}
646
647pub fn miscased_screaming_snake(span: &Span, suggested_name: &str) -> LisetteDiagnostic {
648    LisetteDiagnostic::error("Miscased name")
649        .with_infer_code("constant_not_screaming_snake_case")
650        .with_span_label(span, "expected SCREAMING_SNAKE_CASE")
651        .with_help(format!("Rename to `{}`", suggested_name))
652}
653
654pub fn unused_field(span: &Span) -> LisetteDiagnostic {
655    LisetteDiagnostic::warn("Unused field")
656        .with_lint_code("unused_struct_field")
657        .with_span_label(span, "never read")
658        .with_help("Use or remove this field")
659}
660
661pub fn unused_variant(span: &Span) -> LisetteDiagnostic {
662    LisetteDiagnostic::warn("Unused variant")
663        .with_lint_code("unused_enum_variant")
664        .with_span_label(span, "never constructed or matched")
665        .with_help("Use or remove this enum variant")
666}
667
668pub fn unused_import(span: &Span) -> LisetteDiagnostic {
669    LisetteDiagnostic::warn("Unused import")
670        .with_lint_code("unused_import")
671        .with_span_label(span, "never used")
672        .with_help("Use or remove this import")
673}
674
675pub fn unused_type(span: &Span) -> LisetteDiagnostic {
676    LisetteDiagnostic::warn("Unused type")
677        .with_lint_code("unused_type")
678        .with_span_label(span, "never used")
679        .with_help("Use or remove this type")
680}
681
682pub fn unused_function(span: &Span) -> LisetteDiagnostic {
683    LisetteDiagnostic::warn("Unused function")
684        .with_lint_code("unused_function")
685        .with_span_label(span, "never called")
686        .with_help("Call or remove this function")
687}
688
689pub fn unused_constant(span: &Span) -> LisetteDiagnostic {
690    LisetteDiagnostic::warn("Unused constant")
691        .with_lint_code("unused_constant")
692        .with_span_label(span, "never used")
693        .with_help("Use or remove this constant")
694}
695
696pub fn private_type_in_public_api(
697    span: Option<&Span>,
698    private_type: &str,
699    public_definition: &str,
700) -> LisetteDiagnostic {
701    let mut diagnostic = LisetteDiagnostic::warn(format!(
702        "Private type `{}` in public API",
703        private_type
704    ))
705    .with_lint_code("internal_type_leak")
706    .with_help(format!(
707        "`{}` is private but exposed by `{}`, which is public. Add `pub` to the private type or remove it from the public API",
708        private_type, public_definition
709    ));
710
711    if let Some(s) = span {
712        diagnostic = diagnostic.with_span_label(s, "private");
713    }
714
715    diagnostic
716}
717
718pub fn unknown_attribute(span: &Span, name: &str, known: &[&str]) -> LisetteDiagnostic {
719    let known_list = known
720        .iter()
721        .map(|attribute| format!("`#[{attribute}]`"))
722        .collect::<Vec<_>>()
723        .join(", ");
724    LisetteDiagnostic::warn("Unknown attribute")
725        .with_lint_code("unknown_attribute")
726        .with_span_label(span, "not recognized")
727        .with_help(format!(
728            "`{name}` is not a recognized attribute. Known attributes: {known_list}"
729        ))
730}
731
732pub fn tag_has_alias(span: &Span, key: &str) -> LisetteDiagnostic {
733    LisetteDiagnostic::info("Prefer predefined tag alias")
734        .with_lint_code("tag_has_alias")
735        .with_span_label(span, "use alias instead")
736        .with_help(format!(
737            "Use `#[{}(...)]` instead of `#[tag(...)]` for better validation",
738            key
739        ))
740}
741
742pub fn unknown_tag_option(span: &Span, option: &str) -> LisetteDiagnostic {
743    LisetteDiagnostic::warn("Unknown tag option")
744        .with_lint_code("unknown_tag_option")
745        .with_span_label(span, "not recognized")
746        .with_help(format!(
747            "`{}` is not a recognized tag option. Known options: `snake_case`, `camel_case`, `omitempty`, `!omitempty`, `skip`, `string`",
748            option
749        ))
750}
751
752pub fn trim_charset_misuse(span: &Span, function: &str) -> LisetteDiagnostic {
753    LisetteDiagnostic::warn(format!("Misuse of `{function}`"))
754        .with_lint_code("trim_charset_misuse")
755        .with_span_label(span, "treated as charset")
756        .with_help(format!(
757            "`strings.{function}` removes a set of characters, not a substring. Did you mean `strings.TrimPrefix` or `strings.TrimSuffix`?"
758        ))
759}
760
761pub fn duplicate_arguments(span: &Span, module: &str, function: &str) -> LisetteDiagnostic {
762    let display_module = module.strip_prefix("go:").unwrap_or(module);
763    LisetteDiagnostic::warn("Duplicate arguments")
764        .with_lint_code("duplicate_arguments")
765        .with_span_label(span, "identical arguments")
766        .with_help(format!(
767            "Passing the same value twice to `{display_module}.{function}` makes this call a no-op. Did you mean to pass different values?"
768        ))
769}
770
771pub fn manual_equal_fold(
772    span: &Span,
773    negated: bool,
774    namespace: &str,
775    left_arg: &str,
776    right_arg: &str,
777) -> LisetteDiagnostic {
778    let prefix = if negated { "!" } else { "" };
779    LisetteDiagnostic::info("Inefficient comparison")
780        .with_lint_code("manual_equal_fold")
781        .with_span_label(span, "can use `strings.EqualFold`")
782        .with_help(format!(
783            "Use `{prefix}{namespace}.EqualFold({left_arg}, {right_arg})` to compare case-insensitively in one call"
784        ))
785}
786
787pub fn manual_bytes_equal(
788    span: &Span,
789    negated: bool,
790    namespace: &str,
791    left_arg: &str,
792    right_arg: &str,
793) -> LisetteDiagnostic {
794    let prefix = if negated { "!" } else { "" };
795    LisetteDiagnostic::info("Manual `bytes.Equal`")
796        .with_lint_code("manual_bytes_equal")
797        .with_span_label(span, "can use `bytes.Equal`")
798        .with_help(format!(
799            "Use `{prefix}{namespace}.Equal({left_arg}, {right_arg})` to compare byte slices directly"
800        ))
801}
802
803pub fn redundant_sprintf(span: &Span, namespace: &str, value: &str) -> LisetteDiagnostic {
804    LisetteDiagnostic::info("Redundant `Sprintf`")
805        .with_lint_code("redundant_sprintf")
806        .with_span_label(span, "returns its argument unchanged")
807        .with_help(format!(
808            "`{namespace}.Sprintf(\"%s\", {value})` formats a string as itself. Use `{value}` directly"
809        ))
810}
811
812pub fn manual_replace_all(
813    span: &Span,
814    namespace: &str,
815    s: &str,
816    old: &str,
817    new: &str,
818) -> LisetteDiagnostic {
819    LisetteDiagnostic::info("Manual `strings.ReplaceAll`")
820        .with_lint_code("manual_replace_all")
821        .with_span_label(span, "can use `strings.ReplaceAll`")
822        .with_help(format!(
823            "`{namespace}.Replace({s}, {old}, {new}, -1)` replaces every occurrence. Use `{namespace}.ReplaceAll({s}, {old}, {new})`"
824        ))
825}
826
827pub fn manual_time_since(span: &Span, namespace: &str, arg: &str) -> LisetteDiagnostic {
828    LisetteDiagnostic::info("Manual `time.Since`")
829        .with_lint_code("manual_time_since")
830        .with_span_label(span, "can use `time.Since`")
831        .with_help(format!(
832            "`{namespace}.Since({arg})` is shorthand for `{namespace}.Now().Sub({arg})`"
833        ))
834}
835
836pub fn manual_time_until(span: &Span, namespace: &str, receiver: &str) -> LisetteDiagnostic {
837    LisetteDiagnostic::info("Manual `time.Until`")
838        .with_lint_code("manual_time_until")
839        .with_span_label(span, "can use `time.Until`")
840        .with_help(format!(
841            "`{namespace}.Until({receiver})` is shorthand for `{receiver}.Sub({namespace}.Now())`"
842        ))
843}
844
845pub fn lost_query_mutation(span: &Span, method: &str) -> LisetteDiagnostic {
846    LisetteDiagnostic::warn("Lost query mutation")
847        .with_lint_code("lost_query_mutation")
848        .with_span_label(span, "mutates a discarded copy")
849        .with_help(format!(
850            "`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."
851        ))
852}
853
854pub fn waitgroup_add_in_task(span: &Span) -> LisetteDiagnostic {
855    LisetteDiagnostic::warn("`WaitGroup.Add` inside a `task`")
856        .with_lint_code("waitgroup_add_in_task")
857        .with_span_label(span, "may run after `Wait`")
858        .with_help(
859            "Prefer `wg.Go(|| ...)`, which counts the task and starts it in one step and runs `Done` for you, or move `Add` before the `task`",
860        )
861}
862
863pub fn unnecessary_range_loop(span: &Span, collection: &str) -> LisetteDiagnostic {
864    LisetteDiagnostic::info("Unnecessary range loop")
865        .with_lint_code("unnecessary_range_loop")
866        .with_span_label(span, "can be simpler")
867        .with_help(format!(
868            "This loop exposes the index only to access elements of `{collection}`. Iterate directly over the elements with `for value in {collection}`"
869        ))
870}
871
872pub fn out_of_domain_value(
873    span: &Span,
874    type_display: &str,
875    valid_display: &str,
876) -> LisetteDiagnostic {
877    LisetteDiagnostic::warn("Out-of-domain value")
878        .with_lint_code("out_of_domain_value")
879        .with_span_label(span, "out of domain")
880        .with_help(format!(
881            "`{type_display}` has a closed domain (`{valid_display}`) that excludes this value"
882        ))
883}
884
885pub fn embed_over_impl(span: &Span) -> LisetteDiagnostic {
886    LisetteDiagnostic::info("Interface embedding uses `embed`")
887        .with_lint_code("embed_over_impl")
888        .with_span_label(span, "write `embed` here")
889        .with_help("Interfaces embed other interfaces with `embed`. Replace `impl` with `embed`")
890}