1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
use rustc_ast::Attribute;
use rustc_lint::{EarlyContext, EarlyLintPass, LintStore};
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::{Span, Symbol, sym};
use crate::common::{DefaultState, resolved_state};
mod emit;
mod insertion;
mod scan;
declare_tool_lint! {
/// ### What it does
///
/// When a lint-level attribute (`#[allow]`, `#[expect]`, `#[warn]`,
/// `#[deny]`, `#[forbid]`) carries a trailing `// ...` line comment
/// — on the same source line as the attribute's closing `]` —
/// that documents *why* the level was chosen, lifts the comment
/// into the attribute's `reason = "..."` field and removes the
/// original comment.
///
/// Only the trailing placement counts: a same-line comment after
/// `]` is unambiguously about the attribute. A comment on the
/// *preceding* line is intentionally out of scope — it is just as
/// often documentation for the next item as it is attribute
/// rationale, and a static check cannot tell the two apart.
///
/// Doc comments (`///`, `//!`) and block comments (`/* ... */`)
/// are out of scope.
///
/// ### Why restrict this?
///
/// This is a stylistic preference, not a correctness issue.
/// `reason = "..."` is part of the attribute and travels with it
/// through every refactor; a free-floating comment can be
/// separated from its attribute by an unrelated edit. Compiler
/// diagnostics render the `reason` field in the lint's message,
/// so the rationale reaches the reader at the moment of confusion.
/// One canonical location for the rationale also removes the
/// "is this comment for the attribute, or for the next item?"
/// question.
///
/// ### Example
///
/// **Avoid:**
///
/// ```rust,ignore
/// #[allow(clippy::too_many_arguments)] // matches upstream signature
/// fn build_fetcher(/* ... */) {}
/// ```
///
/// **Prefer:**
///
/// ```rust,ignore
/// #[allow(clippy::too_many_arguments, reason = "matches upstream signature")]
/// fn build_fetcher(/* ... */) {}
/// ```
pub perfectionist::LINT_REASON_FROM_COMMENT,
Warn,
r#"trailing comment on a lint-level attribute should be lifted into a `reason = "..."` field"#,
report_in_external_macro: false
}
const CONFIG_KEY: &str = "perfectionist::lint_reason_from_comment";
/// The rule has no options. The empty struct still exists so that a
/// stray `[perfectionist::lint_reason_from_comment]` table in
/// `dylint.toml` deserialises rather than producing a confusing
/// parse error, and so the generated catalogue renders a
/// `Configuration: none.` entry consistent with the other
/// option-free rules.
#[derive(Debug, Default, serde::Deserialize)]
#[serde(default, deny_unknown_fields, rename_all = "snake_case")]
struct Config {}
pub struct LintReasonFromComment {
/// Outer span of the enclosing `#[cfg_attr(...)]`, carried from
/// its `cfg_attr_trace` to the synthesised inner lint-level
/// attribute so the latter anchors its comment search to the
/// original source rather than its own narrow span. See
/// [`Self::check_attribute`] for the mechanism.
pending_cfg_attr_outer: Option<Span>,
}
impl LintReasonFromComment {
fn new() -> Self {
let _config: Config = dylint_linting::config_or_default(CONFIG_KEY);
Self {
pending_cfg_attr_outer: None,
}
}
}
impl_lint_pass!(LintReasonFromComment => [LINT_REASON_FROM_COMMENT]);
pub fn register_lint(lint_store: &mut LintStore) {
lint_store.register_lints(&[LINT_REASON_FROM_COMMENT]);
}
pub fn register_pass(lint_store: &mut LintStore) {
if let DefaultState::Inactive = resolved_state("lint_reason_from_comment", DefaultState::Active)
{
return;
}
lint_store.register_early_pass(|| Box::new(LintReasonFromComment::new()));
}
const LINT_LEVEL_NAMES: [Symbol; 5] = [sym::allow, sym::expect, sym::warn, sym::deny, sym::forbid];
fn is_lint_level_attribute_name(name: Option<Symbol>) -> bool {
name.is_some_and(|name| LINT_LEVEL_NAMES.contains(&name))
}
impl EarlyLintPass for LintReasonFromComment {
fn check_attribute(&mut self, lint_context: &EarlyContext<'_>, attribute: &Attribute) {
// For an applied `#[cfg_attr(<cond>, <inner>)]`, rustc emits a
// `cfg_attr_trace` attribute (whose span still covers the
// original `#[cfg_attr(...)]`) followed by the synthesised
// `<inner>` attribute(s). The trace's args are an opaque
// `AttrItemKind::Parsed(CfgAttrTrace)` the public `Attribute`
// API can't walk, but its outer *span* is available — so we
// stash it for the synth inner attribute to anchor its
// comment search to, instead of its own narrower span.
if attribute.has_name(sym::cfg_attr_trace) {
// Nested cfg_attr: a `#[cfg_attr(<a>, cfg_attr(<b>, allow(...)))]`
// produces an outer trace, then an inner trace, then the synth
// `allow(...)`. The inner trace's span covers only the inner
// `cfg_attr(<b>, allow(...))` and would miss a trailing comment
// on the outermost `]`. Preserve the outermost trace when its
// span already encloses the new one.
match self.pending_cfg_attr_outer {
Some(existing) if existing.contains(attribute.span) => {}
_ => self.pending_cfg_attr_outer = Some(attribute.span),
}
return;
}
let outer_span = match self.pending_cfg_attr_outer {
Some(trace_span) if trace_span.contains(attribute.span) => trace_span,
_ => {
self.pending_cfg_attr_outer = None;
attribute.span
}
};
if is_lint_level_attribute_name(attribute.name())
&& let Some(args) = attribute.meta_item_list()
{
let emitted = self.check(lint_context, outer_span, attribute.span, &args);
// One `cfg_attr` is the anchor for one adjacent comment. If
// it expands to several lint-level synth attrs
// (`#[cfg_attr(all(), allow(a), warn(b))]`), only the first
// to actually emit consumes the trace — otherwise each
// would delete the same comment bytes and rustfix would
// reject the overlapping edits. Gating on `emitted` keeps
// the trace live when `check` declines (synth already has a
// `reason`, or no comment) so a later synth can still find it.
if emitted && self.pending_cfg_attr_outer == Some(outer_span) {
self.pending_cfg_attr_outer = None;
}
}
}
}