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 non_negative_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("non_negative_comparison")
291 .with_span_label(span, format!("always {result}"))
292 .with_help(
293 "A length is never negative, so this comparison always has the same result. Did you mean to compare against a different value?",
294 )
295}
296
297pub fn redundant_operation(span: &Span, always: Option<&str>) -> LisetteDiagnostic {
298 match always {
299 Some(value) => LisetteDiagnostic::info(format!("Operation always evaluates to `{value}`"))
300 .with_lint_code("redundant_operation")
301 .with_span_label(span, format!("always `{value}`"))
302 .with_help(format!("Simplify this operation to `{value}`")),
303 None => LisetteDiagnostic::info("Operation has no effect")
304 .with_lint_code("redundant_operation")
305 .with_span_label(span, "has no effect")
306 .with_help("Simplify this operation to its other operand"),
307 }
308}
309
310pub fn verbose_failure_propagation(span: &Span) -> LisetteDiagnostic {
311 LisetteDiagnostic::info("Verbose failure propagation")
312 .with_lint_code("verbose_failure_propagation")
313 .with_span_label(span, "verbose")
314 .with_help("Use `?` to propagate the failure concisely")
315}
316
317pub fn self_assignment(span: &Span) -> LisetteDiagnostic {
318 LisetteDiagnostic::warn("Self-assignment")
319 .with_lint_code("self_assignment")
320 .with_span_label(span, "assigning to itself")
321 .with_help("Correct this assignment")
322}
323
324pub fn manual_compound_assignment(span: &Span, symbol: &str) -> LisetteDiagnostic {
325 LisetteDiagnostic::info("Manual compound assignment")
326 .with_lint_code("manual_compound_assignment")
327 .with_span_label(span, "can be simpler")
328 .with_help(format!("Use the `{symbol}` compound assignment operator"))
329}
330
331pub fn manual_is_empty(span: &Span, replacement: &str) -> LisetteDiagnostic {
332 LisetteDiagnostic::info("Length comparison can use `is_empty()`")
333 .with_lint_code("manual_is_empty")
334 .with_span_label(span, "can be simpler")
335 .with_help(format!("Simplify to `{replacement}`"))
336}
337
338pub fn duplicate_logical_operand(span: &Span, operand_text: &str) -> LisetteDiagnostic {
339 LisetteDiagnostic::warn("Duplicate logical operand")
340 .with_lint_code("duplicate_logical_operand")
341 .with_span_label(span, "accidental repetition")
342 .with_help(format!("Simplify to `{operand_text}`"))
343}
344
345pub fn bool_literal_comparison(span: &Span, replacement: &str) -> LisetteDiagnostic {
346 LisetteDiagnostic::info("Redundant comparison to boolean literal")
347 .with_lint_code("bool_literal_comparison")
348 .with_span_label(span, "can be simpler")
349 .with_help(format!("Simplify to `{replacement}`"))
350}
351
352pub fn loop_runs_once(span: &Span) -> LisetteDiagnostic {
353 LisetteDiagnostic::warn("Loop runs at most once")
354 .with_lint_code("loop_runs_once")
355 .with_span_label(span, "the body always exits before looping back")
356 .with_help(
357 "The body always exits on the first iteration, so the loop never repeats. Make the exit conditional, or remove the loop.",
358 )
359}
360
361pub fn unnecessary_return(span: &Span) -> LisetteDiagnostic {
362 LisetteDiagnostic::info("Unnecessary `return`")
363 .with_lint_code("unnecessary_return")
364 .with_span_label(span, "redundant in tail position")
365 .with_help("The final expression of a function is its return value. Drop `return` and keep the value")
366}
367
368pub fn identical_if_branches(span: &Span) -> LisetteDiagnostic {
369 LisetteDiagnostic::warn("Identical if-else branches")
370 .with_lint_code("identical_if_branches")
371 .with_span_label(span, "both branches are equivalent")
372 .with_help("Remove the `if` and keep a single copy of the branch body")
373}
374
375pub fn identical_match_arms(span: &Span) -> LisetteDiagnostic {
376 LisetteDiagnostic::warn("Identical match arms")
377 .with_lint_code("identical_match_arms")
378 .with_span_label(span, "every arm is identical")
379 .with_help(
380 "All `match` arms resolve to the same value. Did you mean for the arms to differ?",
381 )
382}
383
384pub fn unnecessary_bool(span: &Span, consequence_is_true: bool) -> LisetteDiagnostic {
385 let help = if consequence_is_true {
386 "Replace this `if... else` with the condition itself"
387 } else {
388 "Replace this `if... else` with the negated condition"
389 };
390
391 LisetteDiagnostic::info("Unnecessary boolean if-else")
392 .with_lint_code("unnecessary_bool")
393 .with_span_label(span, "can be simpler")
394 .with_help(help)
395}
396
397pub fn redundant_pattern_matching(span: &Span, predicate: &str) -> LisetteDiagnostic {
398 LisetteDiagnostic::info("Redundant pattern matching")
399 .with_lint_code("redundant_pattern_matching")
400 .with_span_label(span, "can be simpler")
401 .with_help(format!("Replace this `match` with `.{predicate}()`"))
402}
403
404pub fn manual_map(span: &Span) -> LisetteDiagnostic {
405 LisetteDiagnostic::info("Manual map")
406 .with_lint_code("manual_map")
407 .with_span_label(span, "can be simpler")
408 .with_help("Replace this `match` with `.map(...)`")
409}
410
411pub fn manual_unwrap_or(span: &Span, default_has_effects: bool) -> LisetteDiagnostic {
412 let method = if default_has_effects {
413 "unwrap_or_else"
414 } else {
415 "unwrap_or"
416 };
417 LisetteDiagnostic::info("Manual `unwrap_or`")
418 .with_lint_code("manual_unwrap_or")
419 .with_span_label(span, "can be simpler")
420 .with_help(format!("Replace this `match` with `.{method}(...)`"))
421}
422
423pub fn redundant_closure(span: &Span, callee: &str) -> LisetteDiagnostic {
424 LisetteDiagnostic::info("Redundant closure")
425 .with_lint_code("redundant_closure")
426 .with_span_label(span, "can be simpler")
427 .with_help(format!("Replace this closure with `{callee}`"))
428}
429
430pub fn empty_match_arm(span: &Span) -> LisetteDiagnostic {
431 LisetteDiagnostic::warn("Empty match arm")
432 .with_lint_code("empty_match_arm")
433 .with_span_label(span, "forgotten stub?")
434 .with_help("Return `()` to indicate an intentional no-op in a match arm")
435}
436
437pub fn unnecessary_parens(span: &Span, keyword: &str) -> LisetteDiagnostic {
438 LisetteDiagnostic::info("Unnecessary parens")
439 .with_lint_code("excess_parens_on_condition")
440 .with_span_label(span, "remove parens")
441 .with_help(format!(
442 "Lisette does not require parens around `{}` conditions",
443 keyword
444 ))
445}
446
447pub fn match_on_literal(span: &Span) -> LisetteDiagnostic {
448 LisetteDiagnostic::warn("Ineffective match")
449 .with_lint_code("match_on_literal")
450 .with_span_label(span, "already known")
451 .with_help(
452 "Matching on a literal is ineffective, because this always succeeds. Did you mean to match on a variable?",
453 )
454}
455
456pub fn single_arm_match(span: &Span, pattern_suggestion: &str) -> LisetteDiagnostic {
457 LisetteDiagnostic::info("Ineffective match")
458 .with_lint_code("single_arm_match")
459 .with_span_label(span, "should be `if let`")
460 .with_help(format!(
461 "A match with a single meaningful arm is ineffective. Use `if let {} = value {{ ... }}` instead.",
462 pattern_suggestion
463 ))
464}
465
466pub fn match_on_bool(span: &Span) -> LisetteDiagnostic {
467 LisetteDiagnostic::info("Match on boolean")
468 .with_lint_code("match_on_bool")
469 .with_span_label(span, "should be `if`")
470 .with_help("A `match` on a boolean is better written as an `if` expression")
471}
472
473pub fn match_single_binding(span: &Span, binding: &str) -> LisetteDiagnostic {
474 LisetteDiagnostic::info("Ineffective match")
475 .with_lint_code("match_single_binding")
476 .with_span_label(span, "should be `let`")
477 .with_help(format!(
478 "A match with a single binding is ineffective. Use `let {} = value` instead.",
479 binding
480 ))
481}
482
483pub fn let_and_return(span: &Span) -> LisetteDiagnostic {
484 LisetteDiagnostic::info("Redundant binding before return")
485 .with_lint_code("let_and_return")
486 .with_span_label(span, "bound and immediately returned")
487 .with_help("Return the value directly instead of binding it first")
488}
489
490pub fn uninterpolated_fstring(span: &Span) -> LisetteDiagnostic {
491 LisetteDiagnostic::info("Uninterpolated f-string")
492 .with_lint_code("uninterpolated_fstring")
493 .with_span_label(span, "zero interpolations")
494 .with_help("Remove the `f` prefix. A string without interpolations does not need to be a format string")
495}
496
497pub fn unnecessary_raw_string(span: &Span) -> LisetteDiagnostic {
498 LisetteDiagnostic::info("Unnecessary raw string")
499 .with_lint_code("unnecessary_raw_string")
500 .with_span_label(span, "no backslashes")
501 .with_help("Remove the `r` prefix. A string without backslashes does not need to be raw")
502}
503
504pub fn invisible_in_string(
505 span: &Span,
506 codepoint: u32,
507 name: &str,
508 is_bidi: bool,
509) -> LisetteDiagnostic {
510 let (title, code, help) = if is_bidi {
511 (
512 "Bidirectional character in string",
513 "bidi_in_string",
514 "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.",
515 )
516 } else {
517 (
518 "Invisible character in string",
519 "invisible_in_string",
520 "Invisible characters in strings can hide bugs and silently shift meaning. Remove the character, or replace it with the visible character you meant.",
521 )
522 };
523 LisetteDiagnostic::warn(title)
524 .with_lint_code(code)
525 .with_span_label(span, format!("contains U+{codepoint:04X} ({name})"))
526 .with_help(help)
527}
528
529pub fn expression_only_fstring(span: &Span) -> LisetteDiagnostic {
530 LisetteDiagnostic::info("Expression-only f-string")
531 .with_lint_code("expression_only_fstring")
532 .with_span_label(span, "the entire f-string is an expression")
533 .with_help("Use the expression directly. Wrapping it in an f-string adds no value")
534}
535
536pub fn rest_only_slice_pattern(span: &Span, help: impl Into<String>) -> LisetteDiagnostic {
537 LisetteDiagnostic::info("Ineffective pattern")
538 .with_lint_code("rest_only_slice_pattern")
539 .with_span_label(span, "always matches")
540 .with_help(help)
541}
542
543pub fn miscased_pascal(span: &Span, code: &str, suggested_name: &str) -> LisetteDiagnostic {
544 LisetteDiagnostic::warn("Miscased name")
545 .with_lint_code(code)
546 .with_span_label(span, "expected PascalCase")
547 .with_help(format!("Rename to `{}`", suggested_name))
548}
549
550pub fn miscased_snake(span: &Span, code: &str, suggested_name: &str) -> LisetteDiagnostic {
551 LisetteDiagnostic::warn("Miscased name")
552 .with_lint_code(code)
553 .with_span_label(span, "expected snake_case")
554 .with_help(format!("Rename to `{}`", suggested_name))
555}
556
557pub fn miscased_screaming_snake(span: &Span, suggested_name: &str) -> LisetteDiagnostic {
558 LisetteDiagnostic::error("Miscased name")
559 .with_infer_code("constant_not_screaming_snake_case")
560 .with_span_label(span, "expected SCREAMING_SNAKE_CASE")
561 .with_help(format!("Rename to `{}`", suggested_name))
562}
563
564pub fn unused_field(span: &Span) -> LisetteDiagnostic {
565 LisetteDiagnostic::warn("Unused field")
566 .with_lint_code("unused_struct_field")
567 .with_span_label(span, "never read")
568 .with_help("Use or remove this field")
569}
570
571pub fn unused_variant(span: &Span) -> LisetteDiagnostic {
572 LisetteDiagnostic::warn("Unused variant")
573 .with_lint_code("unused_enum_variant")
574 .with_span_label(span, "never constructed or matched")
575 .with_help("Use or remove this enum variant")
576}
577
578pub fn unused_import(span: &Span) -> LisetteDiagnostic {
579 LisetteDiagnostic::warn("Unused import")
580 .with_lint_code("unused_import")
581 .with_span_label(span, "never used")
582 .with_help("Use or remove this import")
583}
584
585pub fn unused_type(span: &Span) -> LisetteDiagnostic {
586 LisetteDiagnostic::warn("Unused type")
587 .with_lint_code("unused_type")
588 .with_span_label(span, "never used")
589 .with_help("Use or remove this type")
590}
591
592pub fn unused_function(span: &Span) -> LisetteDiagnostic {
593 LisetteDiagnostic::warn("Unused function")
594 .with_lint_code("unused_function")
595 .with_span_label(span, "never called")
596 .with_help("Call or remove this function")
597}
598
599pub fn unused_constant(span: &Span) -> LisetteDiagnostic {
600 LisetteDiagnostic::warn("Unused constant")
601 .with_lint_code("unused_constant")
602 .with_span_label(span, "never used")
603 .with_help("Use or remove this constant")
604}
605
606pub fn private_type_in_public_api(
607 span: Option<&Span>,
608 private_type: &str,
609 public_definition: &str,
610) -> LisetteDiagnostic {
611 let mut diagnostic = LisetteDiagnostic::warn(format!(
612 "Private type `{}` in public API",
613 private_type
614 ))
615 .with_lint_code("internal_type_leak")
616 .with_help(format!(
617 "`{}` is private but exposed by `{}`, which is public. Add `pub` to the private type or remove it from the public API",
618 private_type, public_definition
619 ));
620
621 if let Some(s) = span {
622 diagnostic = diagnostic.with_span_label(s, "private");
623 }
624
625 diagnostic
626}
627
628pub fn unknown_attribute(span: &Span, name: &str, known: &[&str]) -> LisetteDiagnostic {
629 let known_list = known
630 .iter()
631 .map(|attribute| format!("`#[{attribute}]`"))
632 .collect::<Vec<_>>()
633 .join(", ");
634 LisetteDiagnostic::warn("Unknown attribute")
635 .with_lint_code("unknown_attribute")
636 .with_span_label(span, "not recognized")
637 .with_help(format!(
638 "`{name}` is not a recognized attribute. Known attributes: {known_list}"
639 ))
640}
641
642pub fn tag_has_alias(span: &Span, key: &str) -> LisetteDiagnostic {
643 LisetteDiagnostic::info("Prefer predefined tag alias")
644 .with_lint_code("tag_has_alias")
645 .with_span_label(span, "use alias instead")
646 .with_help(format!(
647 "Use `#[{}(...)]` instead of `#[tag(...)]` for better validation",
648 key
649 ))
650}
651
652pub fn unknown_tag_option(span: &Span, option: &str) -> LisetteDiagnostic {
653 LisetteDiagnostic::warn("Unknown tag option")
654 .with_lint_code("unknown_tag_option")
655 .with_span_label(span, "not recognized")
656 .with_help(format!(
657 "`{}` is not a recognized tag option. Known options: `snake_case`, `camel_case`, `omitempty`, `!omitempty`, `skip`, `string`",
658 option
659 ))
660}
661
662pub fn trim_charset_misuse(span: &Span, function: &str) -> LisetteDiagnostic {
663 LisetteDiagnostic::warn(format!("Misuse of `{function}`"))
664 .with_lint_code("trim_charset_misuse")
665 .with_span_label(span, "treated as charset")
666 .with_help(format!(
667 "`strings.{function}` removes a set of characters, not a substring. Did you mean `strings.TrimPrefix` or `strings.TrimSuffix`?"
668 ))
669}
670
671pub fn duplicate_arguments(span: &Span, module: &str, function: &str) -> LisetteDiagnostic {
672 let display_module = module.strip_prefix("go:").unwrap_or(module);
673 LisetteDiagnostic::warn("Duplicate arguments")
674 .with_lint_code("duplicate_arguments")
675 .with_span_label(span, "identical arguments")
676 .with_help(format!(
677 "Passing the same value twice to `{display_module}.{function}` makes this call a no-op. Did you mean to pass different values?"
678 ))
679}
680
681pub fn lost_query_mutation(span: &Span, method: &str) -> LisetteDiagnostic {
682 LisetteDiagnostic::warn("Lost query mutation")
683 .with_lint_code("lost_query_mutation")
684 .with_span_label(span, "mutates a discarded copy")
685 .with_help(format!(
686 "`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."
687 ))
688}
689
690pub fn waitgroup_add_in_task(span: &Span) -> LisetteDiagnostic {
691 LisetteDiagnostic::warn("`WaitGroup.Add` inside a `task`")
692 .with_lint_code("waitgroup_add_in_task")
693 .with_span_label(span, "may run after `Wait`")
694 .with_help(
695 "Prefer `wg.Go(|| ...)`, which counts the task and starts it in one step and runs `Done` for you, or move `Add` before the `task`",
696 )
697}
698
699pub fn unnecessary_range_loop(span: &Span, collection: &str) -> LisetteDiagnostic {
700 LisetteDiagnostic::info("Unnecessary range loop")
701 .with_lint_code("unnecessary_range_loop")
702 .with_span_label(span, "can be simpler")
703 .with_help(format!(
704 "This loop exposes the index only to access elements of `{collection}`. Iterate directly over the elements with `for value in {collection}`"
705 ))
706}
707
708pub fn out_of_domain_value(
709 span: &Span,
710 type_display: &str,
711 valid_display: &str,
712) -> LisetteDiagnostic {
713 LisetteDiagnostic::warn("Out-of-domain value")
714 .with_lint_code("out_of_domain_value")
715 .with_span_label(span, "out of domain")
716 .with_help(format!(
717 "`{type_display}` has a closed domain (`{valid_display}`) that excludes this value"
718 ))
719}