Add a new lint UNCONSTRUCTIBLE_PUB_STRUCTS#146440
Conversation
|
r? @davidtwco rustbot has assigned @davidtwco. Use |
UNCONSTRUCTIBLE_PUB_STRUCT to detect unconstructible public structs
|
Would the lint fire on token structs that are public, have private fields, have no public constructor method, but expose a limited number of pre-constructed objects, e.g. through a static that contains an optional token? |
won't fire like private types used in such places |
480b1d7 to
021712b
Compare
|
Nominating for t-lang to decide whether we want this lint, then I'll review the implementation. Also, s/unconstructible/unconstructable. |
UNCONSTRUCTIBLE_PUB_STRUCT to detect unconstructible public structsUNCONSTRUCTABLE_PUB_STRUCT to detect unconstructable public structs
This comment was marked as resolved.
This comment was marked as resolved.
6075d81 to
497ad71
Compare
This comment has been minimized.
This comment has been minimized.
This comment was marked as resolved.
This comment was marked as resolved.
497ad71 to
b3b3a05
Compare
|
This PR was rebased onto a different master 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. |
|
Does this trigger on structs that are intended only as marker types in generic parameters? |
Such types usually have intended private unit fields and this won't trigger on them. This will only trigger types with trivial fields but not be used or cannot be constructed. |
|
Are there examples of code in the wild that is affected by this lint? |
|
I worry about completeness here. If I have something like Musing: what if this was signature-based, say? Could it be phrased as "why is this |
|
We discussed this in today's @rust-lang/lang meeting. Was there any particular motivating use case that led to proposing this? Can you point to some code that motivated this? We wondered about potential corner cases, notably structs that are only constructed in unsafe code. For instance, something using Once those are addressed, we'd like to see the (triaged) results of a crater run with this lint marked as deny-by-default, so we can get an idea of 1) how widespread this is and 2) whether this catches any issues. |
|
Please renominate for lang when these answers are available. |
b3b3a05 to
a1aaeb6
Compare
|
@bors try TODO: |
This comment has been minimized.
This comment has been minimized.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
|
@craterbot run mode=check-only crates=https://crater-reports.s3.amazonaws.com/pr-146440/retry-regressed-list.txt p=1 |
|
👌 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
|
🚧 Experiment ℹ️ Crater is a tool to run experiments across parts of the Rust ecosystem. Learn more |
|
🎉 Experiment
Footnotes
|
I'm curious if you could elaborate on this a bit. Sitting with it,
I see the appeal in the latter, but constructible is the more canonical form. See the ngrams viewer. |
What in my mind was that But I would agree that another explanation is that this lint strips out the ones used in local crate. Then it could keep being
Cool! I'll rename it once lint can be merged in and the name is settled. |
|
Reviewing #146440 (comment), including the subsequent adjustments in #146440 (comment), this looks correct and desirable to me. There's a set of careful mitigations here for the cases we had raised and that were found in the crater run. I don't see anything further to do here. Proposal: Let's do this, as warn-by-default, with the name @rfcbot fcp merge lang |
|
@traviscross has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! cc @rust-lang/lang-advisors: FCP proposed for lang, please feel free to register concerns. |
|
@rfcbot reviewed |
|
https://docs.rs/crate/rocket/latest/source/tests/responder_lifetime-issue-345.rs seems to be triggering this, but the code there looks reasonable? #![allow(dead_code)] // This test is only here so that we can ensure it compiles.
#[macro_use] extern crate rocket;
use rocket::{Request, State};
use rocket::response::{Responder, Result};
struct SomeState;
pub struct CustomResponder<'r, R> {
responder: R,
state: &'r SomeState,
}
impl<'r, 'o: 'r, R: Responder<'r, 'o>> Responder<'r, 'o> for CustomResponder<'r, R> {
fn respond_to(self, req: &'r Request<'_>) -> Result<'o> {
self.responder.respond_to(req)
}
}
#[get("/unit_state")]
fn unit_state(state: &State<SomeState>) -> CustomResponder<()> {
CustomResponder { responder: (), state: &*state }
}
#[get("/string_state")]
fn string_state(state: &State<SomeState>) -> CustomResponder<String> {
CustomResponder { responder: "".to_string(), state: &*state }
}Should this be under dead_code instead of unused? With a T-compiler hat on, if it's not added to dead_code, we should add it to the lints we allow at a CLI level in compiletest ( rust/src/tools/compiletest/src/runtest.rs Line 1872 in 042c759 |
|
👍 for stabilizing this, but under its current name, "unconstructable", with the 'a'. Note on spelling: https://www.merriam-webster.com/dictionary/constructable
This was already changed in response to #146440 (comment) . Let's not churn again. Our existing usage in the repository also trends towards the 'a': (I expect the compiler's spelling-correction logic will always tell the user how to spell it. But perhaps we should just pick a name that doesn't have this problem.) |
|
I think the constructable/constructible spelling thing is going to be annoying to deal with, so I'm +1 on finding a name that doesn't use the word. I haven't thought of any alternatives I like, though. |
I'm happy to pick a name that avoids this, though I don't have any immediate ideas other than, perhaps, Regarding the spelling of constructible:
If we wouldn't misspell destructible, then it would seem odd to me for us to go out of our way to pick the -able variant of constructible when the -ible form is supported by precedent relevant to our use, usage data, and etymology. That would seem unnecessarily inconsistent. The grep of our source code mostly comes back with one test that uses |
Currently,
We already pass |
|
Ah, that’s why I’ve felt like I had seen that word somewhere a bunch of times before! Looking at the updated note+help after bab791c, I don’t think it’s quite right yet…
since adding The way to make it a ZST would not be to “add a field of type Footnotes
|
To me, In other words, I think
Updated the wording |
|
How about |
It's a bit long and doesn't really match the grammatical structure of a lint name. |
|
Personally, I lean toward Can we reach consensus on this, or should we brainstorm on Zulip to see if there’s a better name? |
|
It remains nominated. We'll (likely) consider it in the call next week, unless it's gone into FCP. |
| @@ -12,7 +12,7 @@ | |||
| #![feature(no_core, repr_simd, f16)] | |||
| #![crate_type = "rlib"] | |||
| #![no_core] | |||
| #![allow(asm_sub_register, non_camel_case_types)] | |||
| #![allow(asm_sub_register, non_camel_case_types, unused_unconstructable_pub_structs)] | |||
There was a problem hiding this comment.
We already pass -A unused, and as noted above, dead_code is part of the unused group. However, not all compiletests allow it: at least, //@ run-pass tests, aux crates, and rustfix won't pass it.
I see. I think it would make sense to me to add a -Aunconstructable_pub_structs unconditionally to compiletest, at least as part of landing this PR: it significantly reduces the diff, and I'm not convinced the warnings are useful in the tests I see modified here. We can consider whether to enable it in a separate PR.
There was a problem hiding this comment.
Agreed, in fact I also plan to merge this with allow-by-default first, then make it deny-by-default in a seperate PR.
These tests were modified to check if there is false-positives in existing tests and pass CI and then run the crater.
| /// * It has private fields and no public constructor, so downstream crates | ||
| /// cannot construct a value of the type. | ||
| /// * It is not used locally and does not appear in any reachable path from | ||
| /// external APIs other than the public struct item itself. |
There was a problem hiding this comment.
I would think the "reachable path from external APIs" is what is ruling out this lint applying to code like this:
pub struct Bar(u32); // no lint
pub fn foo(_: Bar) {}but then this code still lints and it seems very similar to the above -- Bar is mentioned in public APIs, albeit all impls on Bar:
#[derive(PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Bar(u32); // yes lint
impl Copy for Bar {}
impl Clone for Bar {
fn clone(&self) -> Self { todo!() }
}and yet if one of those is an inherent impl, then the lint stops applying:
pub struct Bar(u32); // no lint
impl Bar { pub fn foo() {} }or takes Bar as an argument:
pub struct Bar(u32); // no lint
impl From<Bar> for u32 {
fn from(_: Bar) -> u32 { todo!() }
}Can you say more about the reasoning behind why we're applying this lint in some cases but not others? It seems like the "non-constructable" argument ought to apply to all cases here.
There was a problem hiding this comment.
I think it makes sense to apply this to free functions and inherent methods whose signatures depend on that type.
The current implementation is based on methods' signatures have receivers or not. This means only methods like fn foo(&self) or with other kinds of receivers would be defered to analyze (based on the existing two-phase analysis for impls.).
For methods like The analysis here is wrong, see below.impl Bar { pub fn foo() {} }, since the signature does not depend on Bar, we only make the most conservative assumption: the function body may use that type. Unless we pre-collect the defs that can be reached through that function body, but that would be a big change, and I guess it may cause a significant performance regression.
For methods like impl From<Bar> for u32 { fn from(_: Bar) -> u32 { todo!() } }, this may be easy to extend, becasue we could not only check the method has a receiver, but also check if the self type appear in types of params.
For free functions or methods like pub fn foo(_: Bar) {}, I think this could be also extended. But this requires we extend the two-phase analysis for impls to free functions, and records the defs are depent by such functions, because we could have multi params, e.g., pub fn foo(_: Foo, _: Bar) {}.
I think these cases (partly) could be handled later as extensions to this lint, and implemented in separate PRs.
There was a problem hiding this comment.
we only make the most conservative assumption: the function body may use that type. Unless we pre-collect the defs that can be reached through that function body, but that would be a big change, and I guess it may cause a significant performance regression.
Don't we have to look through the body already? I'd expect that to be needed to determine that the local crate isn't constructing the structure. And indeed just mentioning the type wthin a function does silence this lint, even if the constructor is not actually invoked:
pub struct Bar(u32); // no lint
pub fn foo() {
Bar;
}There was a problem hiding this comment.
Sorry for the confusion. After thinking about it more, I don’t think we could extend the lint to cases like impl Bar { pub fn foo() { ... } }.
If Bar::foo is public, then it will propagate to Bar, because Bar::foo depends on Bar (at type-level at least), regardless of whether its body actually uses Bar.
There was a problem hiding this comment.
Sure, but some external crate could also be depending on Bar at a type level. I think it still makes sense that if Bar doesn't look like a special type (heuristics you've put in the conditions to apply this lint under -- ZST/inhabited/etc), then we should tell the user that while the type is in the public API, it's a 'weird' pattern and quite plausibly a bug (e.g., missing API).
I don't see much difference between impl Bar { pub fn foo() {} } and pub fn foo(_: Option<Bar>) {}, personally. Both are callable without constructing Bar, but only one of them triggers the lint.
I'm also seeing that we don't silence the lint in the presence of default trait methods returning the otherwise-hidden struct, e.g. this lints:
pub struct Bar(u32);
pub trait Exposed: Sized {
fn output() -> Self { todo!() }
}
impl Exposed for Bar { }and this lints, which is fully incorrect:
pub struct Bar(u32);
pub trait Exposed: Sized {
fn output() -> Self { todo!() }
}
trait Foo {
fn output() -> Self;
}
impl Foo for Bar {
fn output() -> Self { Bar(0) }
}
impl<T: Foo> Exposed for T {
fn output() -> Self { T::output() }
}I'm guessing it's pretty hard to fix these. In general I think the utility of this lint is not necessarily worth its complexity -- it seems like we need a pretty sophisticated analysis if we want to avoid potential false positives like this. The examples here are a little contrived, but the combination of complicated heuristics and the fact that it's a breaking change to remove the struct if you care about API stability makes me question whether it's a good idea to keep trying to fix the lint.
There was a problem hiding this comment.
I don't see much difference between impl Bar { pub fn foo() {} } and pub fn foo(_: Option) {}, personally. Both are callable without constructing Bar, but only one of them triggers the lint.
They both won't trigger the lint because they both will mark Bar as live (for now). But seems what you said is mainly for the future.
There was a problem hiding this comment.
I'm guessing it's pretty hard to fix these. In general I think the utility of this lint is not necessarily worth its complexity -- it seems like we need a pretty sophisticated analysis if we want to avoid potential false positives like this. The examples here are a little contrived, but the combination of complicated heuristics and the fact that it's a breaking change to remove the struct if you care about API stability makes me question whether it's a good idea to keep trying to fix the lint.
After thinking it over, I agree with your point. I also haven’t come up with a low-complexity way to address the case above.
So the best approach may be to close this PR for now.
| /// cannot construct a value of the type. | ||
| /// * It is not used locally and does not appear in any reachable path from | ||
| /// external APIs other than the public struct item itself. | ||
| /// * It is not uninhabited, is not composed only of ZST fields, and has no unit field. |
There was a problem hiding this comment.
Do we exclude types with repr(C)/repr(transparent) on them as well?
There was a problem hiding this comment.
Yes, I will add the rule into the description
| /// This lint helps find those items. | ||
| /// | ||
| /// To silence the warning for individual items, prefix the name with an | ||
| /// underscore such as `_Foo`. |
There was a problem hiding this comment.
If the type is pre-existing, but can't just be deleted, then renaming it is also highly likely not possible (a breaking change as much as deleting it would be).
If the type is new then naming it _Foo seems like the wrong recommendation: either allowing the lint, adding a ! or () field, making it an empty enum (enum Foo {}) all seem like better options.
I'd prefer we recommend one of those options over the _ prefixing. I think we do that below ("is intentional"), maybe we just delete this line?
| /// never type if the struct is only used at the type level. | ||
| /// | ||
| /// Otherwise, consider removing it if the struct is no longer in use. | ||
| pub UNUSED_UNCONSTRUCTABLE_PUB_STRUCTS, |
There was a problem hiding this comment.
With regards to the name and finding alternatives to unconstructable, how would we feel about focusing on the fields being dead code? Essentially something like unused_private_fields or so?
Alternatively, I think in all cases where we lint on the struct with this lint we're already lint on each individual field with "field is never read", right? So maybe we just extend the help text / messaging on the field-level lints to say that the struct is also not constructable, but omit the lint on the struct itself?
As another option, it seems plausible to me that we also want a lint on uncallable functions that reference such a struct -- maybe we can focus on that, something like unreachable_api?
There was a problem hiding this comment.
Alternatively, I think in all cases where we lint on the struct with this lint we're already lint on each individual field with "field is never read", right? So maybe we just extend the help text / messaging on the field-level lints to say that the struct is also not constructable, but omit the lint on the struct itself?
Currently "field is never read" is a part of dead_code. This means we need to extend the scope of lint dead_code to include such structs, otherwise "field is never read" couldn't cover such structs. And I'm afraid this couldn't be done partly like only for field-level lints, which means we cannot achieve the goal, i.e., "extend the help text / messaging on the field-level lints".
I still prefer providing a separate lint, and keeping the scope of dead_code limited to non-exported items. Then users can allow or deny this lint separately.
As another option, it seems plausible to me that we also want a lint on uncallable functions that reference such a struct -- maybe we can focus on that, something like unreachable_api?
I like this idea! Especially since we could implement the extensions mentioned above in theory. Even with current scope of the lint, I think this name (or other variants) would still work, and would leave room for future extensions!
|
According to #146440 (comment), I plan to close this PR. If one day I find a solution, I'll reopen it (or as a new PR). But now, I believe the underlying assumptions of this PR are unsound. Thanks to everyone who participated! |
|
Thanks @mu001999 for your work on this, and thanks @Mark-Simulacrum for the fantastic edge-case analysis. |
View all comments
Add a new lint
UNCONSTRUCTIBLE_PUB_STRUCTSto detect unused unconstructable public structs, based on the following observations:And, with the similar approach of the lint
dead_code_pub_in_binary, this lint is also independent ofdead_codeanddead_code_pub_in_binary.