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}