Re-port lint attributes to attribute parsers rust-lang/rust#155691

Open

66 comments and reviews loaded in 2.13s

Bryntet Avatar

View all comments

I managed to fix the related issues related to the lint attrs PR (one of which, with the help of @JonathanBrouwer ❤️ )

this is another attempt at #152369 and reverts #155056

r? @JonathanBrouwer
r? @jdonszelmann

follow up to this PR would be a cleaner way to do what I do in the commit c9e832c

probably by adding a new variant to ShouldEmit or changing the Nothing variant to have an option of whether to emit bugs or not

rustbot Avatar

Some changes occurred in compiler/rustc_attr_parsing

cc @jdonszelmann, @JonathanBrouwer

These commits modify the Cargo.lock file. Unintentional changes to Cargo.lock can be introduced when switching branches and rebasing PRs.

If this was unintentional then you should revert the changes before this PR is merged.
Otherwise, you can ignore this comment.

Some changes occurred in compiler/rustc_passes/src/check_attr.rs

cc @jdonszelmann, @JonathanBrouwer

Some changes occurred in compiler/rustc_hir/src/attrs

cc @jdonszelmann, @JonathanBrouwer

Some changes occurred in src/tools/clippy

cc @rust-lang/clippy

JonathanBrouwer Avatar
rust-timer Avatar
rust-timer on 2026-04-23 14:06:39 UTC · hidden as outdated
View on GitHub

Awaiting bors try build completion.

@rustbot label: +S-waiting-on-perf

rust-bors Avatar
rust-bors on 2026-04-23 14:06:43 UTC · hidden as outdated
rust-bors Avatar
rust-bors on 2026-04-23 14:06:43 UTC · hidden as outdated
View on GitHub

⌛ Trying commit a7db19b with merge a8bd7ef

To cancel the try build, run the command @bors try cancel.

Workflow: https://github.com/rust-lang/rust/actions/runs/24839909692

rust-bors Avatar

☀️ Try build successful (CI)
Build commit: a8bd7ef (a8bd7ef876125368f7f163a9b97cd468051ca908, parent: 827651f2200cefab42dac4c2ae7f80a7149340de)

JonathanBrouwer Avatar
rust-timer Avatar
rust-timer on 2026-04-23 16:25:22 UTC · hidden as outdated
View on GitHub

Queued a8bd7ef with parent 827651f, future comparison URL.
There is currently 1 preceding artifact in the queue.
It will probably take at least ~1.1 hours until the benchmark run finishes.

petrochenkov Avatar
tests/pretty/delegation-inherit-attributes.pp
3 #![attr = Feature([fn_delegation#0])]
4 extern crate std;
5 #[attr = PreludeImport]
6 use std::prelude::rust_2021::*;

Why doesn't HIR pretty printing respect the comment ordering anymore?

compiler/rustc_span/src/caching_source_map_view.rs · outdated
142 142 span_data.lo - lo_line_bounds.start,
143 143 hi_line_number,
144 span_data.hi - hi_line_bounds.start,
144 span_data.hi,

all I can come up with is that I accidentally made this change when #152369 rebasing onto some version of main after #154614

I'll remove this change.

compiler/rustc_hir/src/attrs/data_structures.rs · outdated
929 original_name: Option<Symbol>,
930 /// Index of this lint, used to keep track of lint groups
931 lint_index: usize,
932 kind: LintAttrTool,

Any specific reason to make the fields here private and provide trivial accessor methods instead?
Most of the other data in this file is public.

View changes since the review

I mainly just don't want other places constructing LintInstance without using LintInstance::new

I'm fine with making these public though if you think this would be preferable!

compiler/rustc_hir/src/attrs/data_structures.rs · outdated
923 /// The fully resolved name of the lint
924 /// for renamed lints, this gets updated to match the new name
925 lint_name: Symbol,
926 /// The raw identifier for resolving this lint

Nit: "raw identifier" has a specific and different meaning.

View changes since the review

compiler/rustc_hir/src/attrs/data_structures.rs · resolved
1002 if let LintAttrTool::Present { tool_name, .. } = self.kind { Some(tool_name) } else { None }
1003 }
1004
1005 pub fn tool_is_named(&self, other: Symbol) -> bool {

Looks like the method is never used.

Could you check if the other methods are actually used as well?

View changes since the review

compiler/rustc_hir/src/attrs/data_structures.rs · outdated
1017 #[derive(Debug, Clone, PrintAttribute, Encodable, Decodable, HashStable_Generic)]
1018 enum LintAttrTool {
1019 Present { tool_name: Symbol, full_lint: Symbol },
1020 NoTool,
compiler/rustc_hir/src/attrs/data_structures.rs · outdated
963 None => LintAttrTool::NoTool,
964 };
965
966 Self { original_name, span, lint_index, lint_name, kind }

Could you avoid using Self in non-generic contexts? At least in constructors like this.

View changes since the review

would you prefer this in the function signature as well?

Up to you, I think having LintInstance { and LintInstance::new constructors searchable is particularly important, but I personally usually avoid Self in other cases too.

compiler/rustc_hir/src/attrs/data_structures.rs · outdated
941 impl LintInstance {
942 pub fn new(
943 original_name: Symbol,
944 long_lint_name: String,
Suggested change
long_lint_name: String,
long_lint_name: Symbol,

View changes since the review

compiler/rustc_hir/src/attrs/data_structures.rs · outdated
957 };
958 let kind = match tool_name {
959 Some(tool_name) => {
960 let full_lint = Symbol::intern(&format!("{tool_name}::{lint_name}",));

What's the difference between long_lint_name and full_lint?

View changes since the review

petrochenkov Avatar

I'd like to review this PR before it is merged.
I started today and will continue tomorrow.

The PR is huge and contains both various refactorings (registered_tools, early.rs, string -> symbol, etc) and the core change (migrating lint attributes), and perhaps some post-migration cleanups too.
It would be good to split it into at least 2 PRs, one for refactorings (with explanation/motivation for each change) and then another for the core change.

Also cjgilllot left some comments in #152369, didn't check if they were addressed yet.

rust-timer Avatar

Finished benchmarking commit (a8bd7ef): comparison URL.

Overall result: ❌✅ regressions and improvements - please read:

Benchmarking means the PR may be perf-sensitive. It's automatically marked not fit for rolling up. Overriding is possible but disadvised: it risks changing compiler perf.

Next, please: If you can, justify the regressions found in this try perf run in writing along with @rustbot label: +perf-regression-triaged. If not, fix the regressions and do another perf run. Neutral or positive results will clear the label automatically.

@bors rollup=never
@rustbot label: -S-waiting-on-perf +perf-regression

Instruction count

Our most reliable metric. Used to determine the overall result above. However, even this metric can be noisy.

mean range count
Regressions ❌
(primary)
- - 0
Regressions ❌
(secondary)
0.3% [0.2%, 0.6%] 8
Improvements ✅
(primary)
-0.3% [-0.6%, -0.2%] 16
Improvements ✅
(secondary)
-0.4% [-0.5%, -0.2%] 26
All ❌✅ (primary) -0.3% [-0.6%, -0.2%] 16

Max RSS (memory usage)

Results (primary -0.0%, secondary 1.0%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
3.2% [3.2%, 3.2%] 1
Regressions ❌
(secondary)
4.6% [2.0%, 6.5%] 7
Improvements ✅
(primary)
-0.8% [-1.3%, -0.6%] 4
Improvements ✅
(secondary)
-2.5% [-6.9%, -1.1%] 7
All ❌✅ (primary) -0.0% [-1.3%, 3.2%] 5

Cycles

Results (primary 2.9%, secondary 4.3%)

A less reliable metric. May be of interest, but not used to determine the overall result above.

mean range count
Regressions ❌
(primary)
2.9% [2.0%, 4.0%] 7
Regressions ❌
(secondary)
4.3% [2.4%, 6.1%] 13
Improvements ✅
(primary)
- - 0
Improvements ✅
(secondary)
- - 0
All ❌✅ (primary) 2.9% [2.0%, 4.0%] 7

Binary size

This perf run didn't have relevant results for this metric.

Bootstrap: 490.734s -> 492.119s (0.28%)
Artifact size: 394.28 MiB -> 394.31 MiB (0.01%)

JonathanBrouwer Avatar
JonathanBrouwer left a comment · edited
View on GitHub

Will do some more review later, some initial comments

View changes since this review

tests/ui/parser/issues/issue-104620.rs · outdated
2 2
3 #![rustc_dummy=5z] //~ ERROR invalid suffix `z` for number literal
3 #![rustc_dummy=5z]
4 //~^ ERROR invalid suffix `z` for number literal

I think I made this change during a time when I was getting some duplicate errors, and I just forgot to remove this formatting change back to it's original. now it's reverted

tests/ui/lint/rfc-2383-lint-reason/multiple_expect_attrs.rs · outdated
2 2
3 3 #![warn(unused)]
4 4
5 #[warn(unused_variables)]

change has been reverted, I forget why it was changed

src/tools/clippy/clippy_lints/src/returns/needless_return.rs · resolved
186 && let Some(lst) = metas
187 && let [MetaItemInner::MetaItem(meta_item), ..] = lst.as_slice()
188 && let [tool, lint_name] = meta_item.path.segments.as_slice()
189 && tool.ident.name == sym::clippy

The tool.ident.name == sym::clippy is missing from the right-hand side

rustbot Avatar

Reminder, once the PR becomes ready for a review, use @rustbot ready.

JonathanBrouwer Avatar
compiler/rustc_passes/src/check_attr.rs
1499 fn check_unused_attribute(&self, hir_id: HirId, attr: &Attribute) {
1500 // Warn on useless empty attributes.
1501 // FIXME(jdonszelmann): this lint should be moved to attribute parsing, see `AcceptContext::warn_empty_attribute`
1502 let note = if attr.has_any_name(&[sym::feature])

Why is this unused check for feature added here?

View changes since the review

diff looks weird, but it's because I refactored the check_lint_attr logic out of the already existing function check_unused_attribute

I can't find this check anywhere in the old code tho, could you point me to where this check was moved from?

compiler/rustc_passes/src/check_attr.rs · resolved
1525 }
1526 Some(ast::AttrStyle::Inner) | None => self.tcx.emit_node_span_lint(
1442 fn check_lint_attr(&self, hir_id: HirId, sub_attrs: &[LintAttribute]) {
1443 for LintAttribute { attr_span, lint_instances, attr_style, .. } in sub_attrs {

Could you leave a FIXME in place to move this logic to attr_parsing in a future PR?

View changes since the review

doing this in attr_parsing would require access to hir_id and TyCtxt in the attr parser, I don't think this is possible since lint attrs need to be really early parsed.

compiler/rustc_mir_build/src/builder/scope.rs · outdated
1299 1299 }
1300 1300
1301 if self.tcx.hir_attrs(id).iter().any(|attr| Level::from_attr(attr).is_some()) {
1301 if self
compiler/rustc_lint_defs/src/lib.rs · outdated
143 141 fn hash_stable(&self, hcx: &mut Hcx, hasher: &mut StableHasher) {
144 142 match self {
145 LintExpectationId::Stable { hir_id, attr_index, lint_index: Some(lint_index) } => {
143 LintExpectationId::Stable { hir_id, attr_index, lint_index, .. } => {

I think the , .. here and in the function below don't do anything right?

View changes since the review

compiler/rustc_attr_parsing/src/attributes/lint.rs · resolved
303 unreachable!("tools required while parsing attributes");
304 };
305 if tools.is_empty() {
306 unreachable!("tools should never be empty")

Wondering why tools should never be empty?

View changes since the review

because the compiler always registers some tools, very early on, way before

this check is there, so if someone in the future doesn't read the comment on parse_limited for example, and wants to parse lint attrs using it (this function doesn't take in tools), the compiler will ICE and they will realise that they should probably use parse_limited_all_filtered instead

compiler/rustc_attr_parsing/src/interface.rs
33 34 /// context, through which all attributes can be lowered.
34 35 pub struct AttributeParser<'sess, S: Stage = Late> {
35 pub(crate) tools: Vec<Symbol>,
36 pub(crate) tools: Option<&'sess FxIndexSet<Ident>>,

Could you make this change to tools a separate PR that is merged before this one, to reduce the size of this one a bit?

View changes since the review

I'd rather move all the other refactorings too #155691 (comment), so the core change becomes as minimal as possible.

compiler/rustc_hir_typeck/src/expr.rs
76 }
77 }
78 false
72 self.tcx.hir_attrs(id).iter().any(Attribute::has_span_without_desugaring_kind)

This looks like it might partially fix #143094 (for the attrs that has_span_without_desugaring_kind supports)

  • Does it?
  • Why does this change need to be in this PR?

View changes since the review

I created the helper function has_span_without_desugaring_kind because if you don't do this check in a specific way, you'll get ICE, and the same check was done in multiple places, and it would become a big mess if we don't have a helper function

I think that issue could possibly be fixed by expanding on this helper function, it does not fix that specific issue right now, no.

compiler/rustc_lint/src/context.rs
851 }
852 }
853 false
826 self.tcx.hir_attrs(id).iter().any(hir::Attribute::has_span_without_desugaring_kind)

see my response to the comment above

rustbot Avatar
rustbot on 2026-04-24 10:45:58 UTC · hidden as outdated
rustbot Avatar
rustbot on 2026-04-24 10:45:58 UTC · hidden as outdated
View on GitHub

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

rustbot Avatar

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

petrochenkov Avatar
compiler/rustc_attr_parsing/src/interface.rs
231 258 &mut emit_lint,
232 259 );
233 260 }
261 let attr_id = sess.psess.attr_id_generator.mk_attr_id();

This is not good, I'd rather make the attr_id in the context optional.
When processing lint attributes it is always set, so it can be unwrapped there.

View changes since the review

compiler/rustc_attr_parsing/src/interface.rs
140 146
147 /// This method provides the same functionality as [`parse_limited_all`](Self::parse_limited_all) except filtered,
148 /// making sure that only allow-listed symbols are parsed
149 pub fn parse_limited_all_filtered<'a>(

Calling this function is not much simpler than calling the underlying parse_limited_all with an iterator.
If levels.rs calls it multiple times, then it could have a more specialized private helper, but otherwise it would probably be better to reduce the API surface.

View changes since the review

compiler/rustc_attr_parsing/src/parser.rs
150 | [sym::deny]
151 | [sym::expect]
152 | [sym::forbid]
153 | [sym::warn]

What is the special relation between lint attributes and incorrect delimiters?
Why shouldn't the delimiters be reported for lint attributes specifically?

View changes since the review

it's specifically only in the case of when ShouldEmit::Nothing

when we compile a file, the the pre ast expansion linting pass doesn't care about cfg attrs

the only attributes we ever parse during pre ast expansion linting, are the lint attributes

because of the fact that in pre expansion, the lint attrs have the ShouldEmit::Nothing, so that we don't get duplicate error emissions, but since we don't know whether the attribute will actually be checked by later steps, (attribute could be removed by cfg), and the behaviour of ShouldEmit::Nothing.emit_err() is to delay_bug, which leads to a delayed bug which never errors and therefore we get ICE, see issue #154800

I want to make another PR later, making this cleaner, where we add maybe another variant to ShouldEmit that doesn't delay bugs or emit errors

compiler/rustc_attr_parsing/src/attributes/lint.rs
14
15 pub(crate) trait Lint {
16 const KIND: LintAttributeKind;
17 const ATTR_SYMBOL: Symbol = Self::KIND.symbol();

Is ATTR_SYMBOl ever overridden to something other than Self::KIND.symbol()?
If not, then it shouldn't be a trait item.

I'm not actually sure why lint attribute kinds need to exist at type level (as opposed to value level LintAttributeKind enum).

View changes since the review

Is ATTR_SYMBOl ever overridden to something other than Self::KIND.symbol()?
If not, then it shouldn't be a trait item.

will change this

I'm not actually sure why lint attribute kinds need to exist at type level (as opposed to value level LintAttributeKind enum).

Because lints behave differently depending on the order lint attributes were placed on a target,

#[deny(unused)]
#[allow(unused)]
fn (my_var: bool) {
}

gives different output compared to

#[allow(unused)]
#[deny(unused)]
fn (my_var: bool) {
}

and unless we want to add an index number to each variant (only possible by being parsed by the same AttributeParser), and then also add support for AttributeParser::finalize to return multiple different AttributeKind as well as re-sorting the attributes into correct order

alternatively, you could keep the parsers separate, and sort them based on their span location, everytime you want to use them, but as you noted in a different comment, we shouldn't use spans for this, and it's innefficient.

compiler/rustc_attr_parsing/src/attributes/lint.rs
123
124 fn finalize(mut self, _cx: &FinalizeContext<'_, '_, S>) -> Option<AttributeKind> {
125 if !self.lint_attrs.is_empty() {
126 // Sort to ensure correct order operations later

Span ordering shouldn't generally be relied upon for anything besides displaying diagnostics.

View changes since the review

Just tested and this sort actually isn't necessary, which is nice :)

compiler/rustc_lint/src/early.rs
391 &[]
323 392 }
324 393 fn check<'ecx, 'tcx, T: EarlyLintPass>(self, cx: &mut EarlyContextAndPass<'ecx, 'tcx, T>) {
325 walk_list!(cx, visit_attribute, self.1);

I'm not sure what this change achieves.
Pre-expansion lints never want to check node attributes?

View changes since the review

compiler/rustc_lint_defs/src/lib.rs
131 129 }
132 130
133 pub fn set_lint_index(&mut self, new_lint_index: Option<u16>) {
131 pub fn set_lint_index(&mut self, new_lint_index: u16) {

This method is never used now.
get_lint_index isn't used either.

View changes since the review

rust-bors Avatar

☔ The latest upstream changes (presumably #155662) made this pull request unmergeable. Please resolve the merge conflicts.

cjgillot Avatar
cjgillot left a comment · edited
View on GitHub

Part of the complexity in this PR comes from moving tool and lint name validation. Why do those need to be moved? Why not leave them in rustc_lint::level which needs to do this validation anyway?

View changes since this review

Bryntet Avatar

Part of the complexity in this PR comes from moving tool and lint name validation. Why do those need to be moved? Why not leave them in rustc_lint::level which needs to do this validation anyway?

Because clippy otherwise manually parses the level from the AST in multiple places

JonathanBrouwer Avatar

Because clippy otherwise manually parses the level from the AST in multiple places

Can we fix that in a separate PR, and just let clippy do that for now?