Skip to content

RFC: --crate-attr #3791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions text/3791-crate-attr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
- Feature Name: `crate-attr`
- Start Date: 2025-03-16
- RFC PR: [rust-lang/rfcs#3791](https://github.com/rust-lang/rfcs/pull/3791)
- Rust Issue: [rust-lang/rust#138287](https://github.com/rust-lang/rust/issues/138287)

# Summary
[summary]: #summary

`--crate-attr` allows injecting crate-level attributes via the command line.
It is supported by all the major tools: Rustc, Rustdoc, Clippy, and Rustfmt.
Rustdoc extends it to doctests, discussed in further detail below.
It is encouraged, but not required, that external `rustc_driver` tools also support this flag.

# Motivation
[motivation]: #motivation

There are three main motivations.

1. CLI flags are easier to configure for a whole workspace at once.
2. When designing new features, we do not need to choose between attributes and flags; adding an attribute automatically makes it possible to set with a flag.
3. Tools that require a specific attribute can pass that attribute automatically.

Each of these corresponds to a different set of stakeholders. The first corresponds to developers writing Rust code. For this group, as the size of their code increases and they split it into multiple crates, it becomes more and more difficult to configure attributes for the whole workspace; they need to be duplicated into the root of each crate. Some attributes that could be useful to configure workspace-wide:
- `no_std`
- `feature` (in particular, enabling unstable lints for a whole workspace at once)
- [`doc(html_{favicon,logo,playground,root}_url}`][doc-url]
- [`doc(html_no_source)`]
- `doc(attr(...))`

Cargo has features for configuring flags for a workspace (RUSTFLAGS, `target.<name>.rustflags`, `profile.<name>.rustflags`), but no such mechanism exists for crate-level attributes.

Additionally, some existing CLI options could have been useful as attributes. This leads to the second group: Maintainers of the Rust language. Often we need to decide between attributes and flags; either we duplicate features between the two (lints, `crate-name`, `crate-type`), or we make it harder to configure the options for stakeholder group 1.

The third group is the authors of external tools. The [original motivation][impl] for this feature was for Crater, which wanted to enable a rustfix feature in *all* crates it built without having to modify the source code. Other motivations include the currently-unstable [`register-tool`], which with this RFC could be an attribute passed by the external tool (or configured in the workspace), and [build-std], which wants to inject `no_std` into all crates being compiled.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and [build-std], which wants to inject no_std into all crates being compiled

Just as a note, RfL already does this exact thing and they would like to see -Zcrate-attr stabilized as well:

https://github.com/Rust-for-Linux/linux/blob/e6ea10d5dbe082c54add289b44f08c9fcfe658af/scripts/Makefile.build#L238-L239


[impl]: https://github.com/rust-lang/rust/pull/52355
[`register-tool`]: https://github.com/rust-lang/rust/issues/66079#issuecomment-1010266282
[doc-url]: https://doc.rust-lang.org/rustdoc/write-documentation/the-doc-attribute.html#at-the-crate-level
[`doc(html_no_source)`]: https://github.com/rust-lang/rust/issues/75497
[build-std]: https://github.com/rust-lang/rfcs/pull/3791#discussion_r1998684847

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

The `--crate-attr` flag allows you to inject attributes into the crate root.
For example, `--crate-attr=crate_name="test"` acts as if `#![crate_name="test"]` were present before the first source line of the crate root.
Comment on lines +45 to +46
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question (not necessarily for you but the Cursed Code Specialists): do we have any wonky (stable) crate-level attributes like #![crate_name = EXPR] (whose validation was only recently fixed as a breaking change in rust-lang/rust#127581) that an injection like --crate-attr=crate_name=include_str!("crate_name.txt") can lead to... interesting behaviors?

Or perhaps, can crate-root attributes take a "value" that's a macro, possibly line!()?

Copy link
Member Author

@jyn514 jyn514 Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these should be questions for the individual attributes, imo. crate-attr does not try to special-case these in any way, it just forwards the macro invocation which gets rejected later in compilation. i do not think we should try to limit the syntax to things that "look normal".

$ rustc +r2-stage1 src/main.rs -Zcrate-attr=crate_name='line!()'
error: malformed `crate_name` attribute input
 --> <crate attribute>:1:1
  |
1 | #![crate_name=line!()]
  | ^^^^^^^^^^^^^^^^^^^^^^ help: must be of the form: `#![crate_name = "name"]`

(here r2-stage1 is a checkout of rust-lang/rust#138336)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that seems completely fair (if anything, may be QoI issue on specific crate-level attrs)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I'll temporarily leave this thread open in case people do know of any Funny Things, but please resolve this comment chain before FCP as it is not intended to be blocking).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, i misunderstood. you were asking what include! and line! should expand to in this context. my understanding is that we do not currently have any crate-level attributes which accept values, right? i'm tempted to say we just delay defining this until we need to.

Copy link
Member

@fmease fmease Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Jyn that we shouldn't restrict the grammar of those attributes.

To answer the original question however, there are several. E.g., #![doc = …], #![deprecated = …], #![type_length_limit = …]. In these three cases you can use concat!("", line!()) as the RHS which expands to (lineno) "1" when passed via -Zcrate-attr (I'm using concat to essentially get rid of the numeric suffix).

(crate_name, crate_type and recursion_limit are fully locked-down nowadays in the sense that they no longer accept macro calls)

(since a lot of malformed and misplaced attrs are still accepted by rustc and only trigger the warn-by-default lint unused_attributes, you can currently also do things like #![must_use = compile_error!("…")] which isn't that interesting arguably)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rustdoc -Zcrate-attr='doc=concat!("test", line!())' foo.rs gave a doc/foo/index.html containing "test1".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. of these, line!() really does not seem to have a meaning to me, maybe the implementation comes up with "1" but i don't think that's useful. so i'm tempted to ban that particular macro in this context.

include! and friends all seem fine to me, they should be relative to whatever #![doc = include_str!()] in the source is relative to.

Copy link
Member

@jieyouxu jieyouxu Mar 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I was mostly thinking adversarially on what weird combinations this can end up with (e.g. what's prone to break, between a stable + stable feature/functionality interaction). I suppose this may also include the other "meta" ones like column!(). Not sure how to phrase this, but "observable implementation details" -- e.g. that line number 1 being observable through line!() is potentially interesting if it can become depended upon by the user.

Alternatively... explicitly carve out a stability caution/exception where it's explicitly remarked that depending on line!()/column!() etc. is not part of the stability guarantees? Idk how to word this though. It's not a big concern but it can be annoying if it ever comes up.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i still think that all these concerns apply just as much to existing crate-level attributes in source code. if there is a concern here it should be specific to attributes passed via a CLI flag.


To inject multiple attributes, pass `--crate-attr` multiple times.

This feature lets you pass attributes to your whole workspace at once, even if rustc doesn't natively support them as flags.
For example, you could configure `strict_provenance_lints` for all your crates by adding
`build.rustflags = ["--crate-attr=feature(strict_provenance_lints)", "-Wfuzzy_provenance_casts", "-Wlossy_provenance_casts"]`
to `.cargo/config.toml`.

This feature also applies to doctests.
Running (for example) `RUSTDOCFLAGS="--crate-attr='feature(strict_provenance_lints)' -Wfuzzy_provenance_casts" cargo test --doc` will enable `fuzzy_provenance_casts` for all doctests that are run.

(This snippet is adapted from [the unstable book].)

[the unstable book]: https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/crate-attr.html#crate-attr

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

Any crate-level attribute is valid to pass to `--crate-attr`.
Attributes are applied in the order they were given on the command line; so `--crate-attr=warn(unused) --crate-attr=deny(unused)` is equivalent to `deny(unused)`.
`crate-attr` attributes are applied before source code attributes.
For example, the following file, when compiled with `crate-attr=deny(unused)`, does not fail with an error, but only warns:

```rust
#![warn(unused)]
fn foo() {}
```

This means that `crate-attr=deny(unused)` is exactly equivalent to `-D unused`.

Formally, the expansion behaves as follows:

1. The crate is parsed as if `--crate-attr` were not present.
2. The attributes in `--crate-attr` are parsed.
3. The attributes are injected at the top of the crate root.
4. Macro expansion is performed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I don't think this quite matches how expansion actually works -- in particular step 1 in the compiler can't parse the full crate, since that's inherently interleaved with macro expansion. But I don't think that fundamentally changes anything specified here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, it might be a good idea to specify this feature in context of precise operation of today's expansion if only to get ahead of potential underspecification issues if the details of macro expansion were to change in the future. I don't expect this to change the structure much, but it may require author to give a closer read on the parser's code.

(I'd be also fine with this part of RFC being refined to describe what ends up being implemented later down the line…)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated this to be a bit more accurate to how things work today, which I think removes most problems we might hit in the future with regards to expansion ordering etc.


As a consequence, this feature does not affect [shebang parsing], nor can it affect nor be affected by comments that appear on the first source line.

Another consequence is that the argument to `--crate-attr` is syntactically isolated from the rest of the crate; `--crate-attr=/*` is always an error and cannot begin a multi-line comment.

`--crate-attr` is treated as Rust source code, which means that whitespace, block comments, and raw strings are valid: `'--crate-attr= crate_name /*foo bar*/ = r#"my-crate"# '` is equivalent to `--crate-attr=crate_name="my-crate"`.

The argument to `--crate-attr` is treated as-if it were surrounded by `#![ ]`, i.e. it must be an inner attribute and it cannot include multiple attributes, nor can it be any grammar production other than an [`Attr`].

If the attribute is already present in the source code, it behaves exactly as it would if duplicated twice in the source.
For example, duplicating `no_std` is idempotent; duplicating `crate_type` generates both types; and duplicating `crate_name` is idempotent if the names are the same and a hard error otherwise.
It is suggested, but not required, that the implementation not warn on idempotent attributes, even if it would normally warn that duplicate attributes are unused.

`--crate-attr` is also a rustdoc flag. Rustdoc behaves identically to rustc for the main crate being compiled.
For doctests, by default, `--crate-attr` applies to both the main crate and the generated doctest.
This can be overridden for the doctest using `--crate-attr=doc(test(attr(...)))`.
`--crate-attr=doc(...)` attributes never apply to the generated doctest, only to the main crate (with the exception of `doc(test(attr(...)))`, which applies the inner `...` attribute, not the doc attribute itself).

[shebang parsing]: https://doc.rust-lang.org/nightly/reference/input-format.html#shebang-removal
[`Attr`]: https://doc.rust-lang.org/nightly/reference/attributes.html

# Drawbacks
[drawbacks]: #drawbacks

It makes it harder for Rust developers to know whether it's idiomatic to use flags or attributes.
In practice, this has not be a large drawback for `crate_name` and `crate_type`, although for lints perhaps a little more so since they were only recently stabilized in Cargo.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sentence is messed up.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more so should be moreso. otherwise it is correct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, is "In practice, this has not be a large drawback" valid grammar? I don't think I've ever heard that expression, but I'm not a native speaker so idk^^

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.... oops. yes, that should say "been", thanks.


# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

- We could require `--crate-attr=#![foo]` instead. This is more verbose and requires extensive shell quoting, for not much benefit.
- We could disallow comments in the attribute. This perhaps makes the design less surprising, but complicates the implementation for little benefit.
- We could add a syntax for passing multiple attributes in a single CLI flag. We would have to find a syntax that avoids ambiguity *and* that does not mis-parse the data inside string literals (i.e. picking a fixed string, such as `|`, would not work because it has to take quote nesting into account). This greatly complicates the implementation for little benefit.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remark: +1, I think it's perfectly fine (and arguably good) to not support multi-attr in same flag. If anything, multiple --crate-attr flags is way easier to read too, IMO.


This cannot be done in a library or macro. It can be done in an external tool, but only by modifying the source in place, which requires first parsing it, and in general is much more brittle than this approach (for example, preventing the argument from injecting a unterminated block comment, or from injecting a non-attribute grammar production, becomes much harder).

In the author's opinion, having source injected via this mechanism does not make code any harder to read than the existing flags that are already stable (in particular `-C panic` and `--edition` come to mind).

# Prior art
[prior-art]: #prior-art

- HTML allows `<meta http-equiv=...>` to emulate headers, which is very useful for using hosted infra where one does not control the server.

# Unresolved questions
[unresolved-questions]: #unresolved-questions

None to my knowledge.

# Future possibilities
[future-possibilities]: #future-possibilities

This proposal would make it easier to use external tools with [`#![register_tool]`][`register-tool`], since they could be configured for a whole workspace at once instead of individually; and could be configured without modifying the source code.