Skip to content

Permit duplicate imports #141043

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 1 commit into
base: master
Choose a base branch
from

Conversation

jswrenn
Copy link
Member

@jswrenn jswrenn commented May 15, 2025

Consider a crate that depends on both serde (without the derive feature) and serde_derive, and imports serde::Serialize (a trait) and serde_derive::Serialize (a macro). Then, imagine some other crate in a build graph depends on serde with the derive feature; they import both the macro and trait simultaneously with use serde::Serialize. If duplicate imports of the same item are always forbidden, these crates cannot co-exist in the same build-graph; the former crate will fail to build, as its first import (which will now also import the Serialize macro) conflicts with its second import.

This build hazard is confusing — the author of the second crate had no idea that their dependence on the derive feature might be problematic for other crates. The author of the first crate can mitigate the hazard by only glob-importing from proc-macro crates, but glob imports run against many's personal preference and tooling affordances (e.g., rust-analyzer's auto-import feature).

We mitigate this hazard across the ecosystem by permitting duplicate imports of macros. We don't limit this exception to proc macros, as it should not be a breaking change to rewrite a proc macro into a by-example macro. Although it would be semantically unproblematic to permit all duplicate imports (not just those of macros), other kinds of imports have not, in practice, posed the same hazard, and there might be cases we'd like to warn-by-default against. For now, we only permit duplicate macro imports.

See https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/Allowing.20same-name.20imports.20of.20the.20same.20item/near/516777221

r? @compiler-errors

I'm lang-nominating this because I'm not sure if this carve-out rises to the point of requiring an RFC and for the below open questions.

Open Questions

Permitting All Duplicate Identifiers

So long as two bindings resolve to the same item, it's semantically unproblematic for them to have the same name. This PR currently takes the most conservative approach and only carves out macro imports as a case in which duplicate imports of the same item are accepted. However, the warn-by-default unusued_import lint already effectively nudges against duplicate imports. I think we could permit duplicate imports of the the same item — for all kinds of items — without issue.

@jswrenn jswrenn added the I-lang-nominated Nominated for discussion during a lang team meeting. label May 15, 2025
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels May 15, 2025
@rust-log-analyzer

This comment has been minimized.

@jswrenn jswrenn force-pushed the allow-duplicate-macro-imports branch from da5a69d to a978e7c Compare May 15, 2025 17:15
@rust-log-analyzer

This comment has been minimized.

@jswrenn jswrenn force-pushed the allow-duplicate-macro-imports branch from a978e7c to 3241967 Compare May 15, 2025 17:28
@oxalica
Copy link
Contributor

oxalica commented May 15, 2025

Can we have some tests about how does this incorporate with unused_imports? Eg. for use serde::Serialize; use serde_derive::Serialize;, will it warn on #[derive(Serialize)]? Does the use order matter? What about only using the trait fn f(_: &impl Serialize);?
The latter case, only the first use is unambiguously referenced so it should warn as expected. In the former case, the second use is technically unnecessary but there is a reason to keep it.

Consider a crate that depends on both `serde` (without the `derive`
feature) and `serde_derive`, and imports `serde::Serialize` (a trait)
and `serde_derive::Serialize` (a macro). Then, imagine some other crate
in a build graph depends on `serde` *with* the `derive` feature; they
import both the macro and trait simultaneously with `use
serde::Serialize`. If duplicate imports of the same item are always
forbidden, these crates cannot co-exist in the same build-graph; the
former crate will fail to build, as its first import (which will now
also import the `Serialize` macro) conflicts with its second import.

This build hazard is confusing — the author of the second crate had no
idea that their dependence on the `derive` feature might be problematic
for other crates. The author of the first crate can mitigate the hazard
by only glob-importing from proc-macro crates, but glob imports run
against many's personal preference and tooling affordances (e.g.,
`rust-analyzer`'s auto-import feature).

We mitigate this hazard across the ecosystem by permitting duplicate
imports of macros. We don't limit this exception to proc macros, as it
should not be a breaking change to rewrite a proc macro into a
by-example macro. Although it would be semantically unproblematic to
permit *all* duplicate imports (not just those of macros), other kinds
of imports have not, in practice, posed the same hazard, and there might
be cases we'd like to warn-by-default against. For now, we only permit
duplicate macro imports.

See https://rust-lang.zulipchat.com/#narrow/channel/213817-t-lang/topic/Allowing.20same-name.20imports.20of.20the.20same.20item/near/516777221
@jswrenn jswrenn closed this May 16, 2025
@jswrenn jswrenn force-pushed the allow-duplicate-macro-imports branch from 3241967 to 9d97e12 Compare May 16, 2025 13:39
@jswrenn
Copy link
Member Author

jswrenn commented May 16, 2025

Not sure how this got closed...

I've updated tests/ui/proc-macro/duplicate.rs to also test for the interaction with unused_imports. I'd forgotten about that lint, and it might resolve my concerns about allowing all duplicate imports (i.e., not just macro) of the same item. I'll add an 'Open Questions' section to the PR text and leave the question to t-lang.

@jswrenn jswrenn reopened this May 16, 2025
@traviscross traviscross added P-lang-drag-2 Lang team prioritization drag level 2.https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang. T-lang Relevant to the language team needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels May 16, 2025
@traviscross
Copy link
Contributor

However, the warn-by-default unusued_import lint already effectively nudges against duplicate imports. I think we could permit duplicate imports of the the same item — for all kinds of items — without issue.

I agree. And working to specify our language has me in the mood to prefer being consistent where possible rather than adding little carve-outs here and there. So I propose that we allow duplicate imports of the same item for all namespaces.

@rfcbot fcp merge

@rfcbot
Copy link
Collaborator

rfcbot commented May 17, 2025

Team member @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.
See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels May 17, 2025
@traviscross traviscross added the S-waiting-on-documentation Status: Waiting on approved PRs to documentation before merging label May 17, 2025
@petrochenkov petrochenkov self-assigned this May 19, 2025
@petrochenkov
Copy link
Contributor

So long as two bindings resolve to the same item, it's semantically unproblematic for them to have the same name.

Not exactly, the specific reexport chain through which we arrive to the final definition is important.

We currently have some cases in which ambiguities between bindings ultimately having the same Res are ignored, but we are not really doing it correctly in general case.
E.g. ambiguities

// between two globs
pub(in a) use tralala::*; // imports `struct Foo;`
pub(in b) use trulala::*; // also imports `struct Foo;`

// or between outer and inner names in scope under restricted shadowing

are not reported (*), but which visibility (and potentially stability) - a or b will actually be used for privacy checking depends on random internal ordering details of the resolution-expansion algorithm, which is bad.

I'm not sure what the proper rules here should be, and what it takes to implement them, because it has always been a low priority issue. Perhaps support multiple declaration bindings for name uses, and check all of them for privacy/stability/etc.

If we support these ambiguities for single imports as well, we'll expose more of these under-specified/under-implemented parts of language to users.

(*) See the logic in the same fn try_define function as the code added in this PR, a bit above it.

@traviscross
Copy link
Contributor

If we support these ambiguities for single imports as well, we'll expose more of these under-specified/under-implemented parts of language to users.

Do you perhaps have an example of code demonstrating an ambiguity that would be exposed by this that isn't already possible to demonstrate with glob imports, or is the argument here that by exposing more ways to reach these same already-exposed ambiguities, more code in the wild will lean on them?

@petrochenkov
Copy link
Contributor

the argument here that by exposing more ways to reach these same already-exposed ambiguities, more code in the wild will lean on them?

Probably this, I don't think we enable qualitatively more issues than just with globs.

@petrochenkov petrochenkov added S-waiting-on-team Status: Awaiting decision from the relevant subteam (see the T-<team> label). and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels May 21, 2025
@scottmcm
Copy link
Member

I'm wondering if there's new semver implications of this. For example, imagine this scenario:

Upstream crate v1:

pub mod foo { pub fn qux() {} }
pub mod bar { pub use super::foo::qux; }

Downstream crate:

// Not allowed on stable; This change would make it allowed.
use othercrate::bar::qux;
use othercrate::foo::qux;

Upstream crate v2:

pub mod foo { pub fn qux() {} }
pub mod bar { pub fn qux() { super::foo::qux() } }

Now the downstream crate is broken because they're different imports now, so that must have been semver disallowed, and it's not obvious to me that that was already a break.

@workingjubilee
Copy link
Member

@obi1kenobi Hello, as you are an expert on semver breakage in the ecosystem, and thoughts about possible semver breakages were raised in the T-lang meeting, could you weigh in on

  • what issues that have been discussed that you feel are likely to be most problematic?
  • if there are possible semver breakages from this that others have not brought up?
  • if there are issues that we think might be problematic but aren't actually a problem?
  • if this will tend to fix more problems than it solves?

@zachs18
Copy link
Contributor

zachs18 commented May 28, 2025

I'm wondering if there's new semver implications of this. For example, imagine this scenario:

Upstream crate v1:

pub mod foo { pub fn qux() {} }
pub mod bar { pub use super::foo::qux; }

Downstream crate:

// Not allowed on stable; This change would make it allowed.
use othercrate::bar::qux;
use othercrate::foo::qux;

Upstream crate v2:

pub mod foo { pub fn qux() {} }
pub mod bar { pub fn qux() { super::foo::qux() } }

Now the downstream crate is broken because they're different imports now, so that must have been semver disallowed, and it's not obvious to me that that was already a break.

Not sure if this is considered semver-breaking, but that change can already cause breakage, regardless of imports, due to the fact that bar::qux's unique function item type changes from being the same as foo::qux's to being different, e.g. this downstream program will also break when upgrading to upstream v2:

fn main() {
  let mut f = othercrate::foo::qux;
  f = othercrate::bar::qux; // after change: error[E0308]: mismatched types: expected fn item, found a different fn item
}

@scottmcm
Copy link
Member

Hmm, so thinking of item types, same-vs-wrapper is

  • for types: exposed by coherence
  • for functions: exposed by the voldemort type
  • for traits: exposed by the overlap rule, but maybe not exposed if the trait is sealed?
  • for macros: unsure -- is a used macro vs one that forwards the tts to another macro observable?
  • for type aliases: Would this allow also useing something through a type alias if it's the same underlying type, or would it only be looking at the import graph for the check?

@traviscross traviscross changed the title Permit duplicate macro imports Permit duplicate imports May 28, 2025
@obi1kenobi
Copy link
Member

Thanks for the ping! This is a thorny one. As is often the case, making the point-in-time Rust experience better can make the SemVer story much more tricky.

TL;DR:

  • I'm deeply sympathetic to the zerocopy macros problem. It sucks, and we should solve it.
  • I'm cautiously optimistic that allowing duplicate macro imports might be viable. I think we have to be very careful, and invest much more time into considering the edge cases than we have so far, though.
  • Duplicate imports in the general case feels like a non-starter, unless we at minimum completely overhaul type aliases in the process.

This is my thinking after dedicating ~2.5h to this question. I'm reasonably confident that more discussion and effort will produce more things to consider — if I were on lang-advisors I'd want to register a concern for the FCP process.

Exact resolution of duplicate items matters semantically

it's semantically unproblematic for them to have the same name.

Not exactly, the specific reexport chain through which we arrive to the final definition is important.

Strongly agree that the specific reexport chain is important. Consider the following:

mod inner {
    mod defn {
        pub struct Example;
    }

    #[doc(hidden)]
    mod nested {
       pub use super::defn::Example;
    }

    // This PR would allow this:
    #[deprecated]
    pub use defn::Example;
    pub use nested::Example;
}

// Which `Example` are we re-exporting?
// One of them is non-public API,
// the other is deprecated.
//
// User code that depends on non-public API (conceptually) should lint.
// Using deprecated code already does trigger a lint.
// Which lint should fire when users import `Example` from here?
pub use inner::*;

It might be tempting to say something like "just combine the modifiers — it's both deprecated and doc(hidden)" but that's actually incorrect: deprecated doc(hidden) items are public API in Rust! That fact is the "least-bad but at least self-consistent" interpretation of how the two concepts interact. The alternative interpretations either allow major breakage without a SemVer major bump, or de facto rule that common real-world doc(hidden) use cases like "don't show deprecated items on docs.rs" are SemVer-violating.

I would recommend looking through my post Breaking semver in Rust by adding a private type, or by adding an import — some of the examples there are quite cursed, and you'll likely come up with more edge cases based on it.

Explicit vs glob resolution priority considerations (click to expand)

Currently, explicit imports and definitions take priority over glob imports: playground

pub mod inner {
    mod defn {
        pub struct Example;
    }

    mod nested {
        #[allow(non_upper_case_globals)]
        pub const Example: i64 = 1;
    }

    pub use defn::Example;
    pub use nested::*;
}

use inner::Example;

fn main() {
    // This works because `Example` in the values namespace
    // refers to the unit struct, not the `const` item.
    let _x: Example = Example;
}

However we choose to handle duplicate imports of the same item, we have to make sure it's consistent with the existing priority order

Type aliases make everything difficult

Type aliases are also a nightmare — so much so that (for the time being) I gave up on doing any better than the bare minimum analysis on them in cargo-semver-checks.

Aside, to demonstrate how problematic and counter-intuitive type aliases are (click to expand)

Making a pub type alias of a private type is allowed (!) and merely produces a warn-by-default lint:

struct Private;

#[allow(private_interfaces)]
pub type AliasOfPrivate = Private;

If we choose to allow duplicating more items than just macros, we have to grapple with "when is a type alias the same as the underlying type." This is a nightmare of a question.

For example:

pub mod a {
    pub struct Example<T>(T);
}

mod b {
    pub type Alias<T = i64> = super::a::Example<T>;   
}

pub use a::Example as Reexport;

// Should this be allowed?
pub use b::Alias as Reexport;

This is a good example of "improving point-in-time ergonomics can make SemVer hazards worse."

It's tempting to allow the duplicate here: anything Example can do, Alias can do too! We can quite literally find-and-replace every use of Example with a valid path to Alias, and the code will still compile.

This is a nasty SemVer hazard though! Consider the nominally minor, non-breaking change of adding a default value to Example<T>:

-    pub struct Example<T>(T);
+    pub struct Example<T = String>(T);

What generic type does my_crate::Reexport carry now?

  • If we resolve Reexport -> a::Example then T = String.
  • If we resolve Reexport -> b::Alias then T = i64.

None of these choices is obviously correct — especially in a cross-crate setting where not all these items might be defined in the same crate.

Since there's no good choice, we can also deny uses of Reexport without an explicit type argument. We can do so at the point of attempted use (like in some cases with ambiguous glob re-exports), or attempt to do so eagerly at the point where the duplicate import happens. But again, the cross-crate setting means these boil down to the same thing from a SemVer perspective — adding the default triggered a SemVer-major breaking change.

@workingjubilee
Copy link
Member

Considering the number of edge-cases to consider when extending it wider, starting with the ones with Predrag brings up, and I'm pretty sure some wargaming would turn up more? It seems... inadvisable to scope-creep this PR beyond macro imports.

My understanding of how macros work and resolve is that their import/export rules are, in fact, special1. Even if we do say some variation of "allow every case, and damn the consequences! caveat maintainer!" in the end, it doesn't seem advisable to tie throwing the switch for the macro namespace to throwing the switch for the type or value namespaces. We are not going to get them following the exact same ruleset for resolution if we do, because of different special-cases for resolution for each2.

Note the FCP currently proposed is in fact for all namespaces, i.e. macro, type, and value namespaces3. Yet we don't have an answer for questions like how this change would handle type aliases. How this would handle type aliases seems like a very key part, and at least to me is not a triviality to easily decide and tack on. Even if T-lang wanted to pursue the greater proposal of all namespaces, it should perhaps be a new PR?

Footnotes

  1. Because they need to be resolved before you can expand code, and they have their own namespace, to begin with, plus the special prelude...

  2. e.g. #[macro_use] and #[macro_export] for macros and type Alias = Type; for types.

  3. I am pretty sure the lifetime and label namespaces are not actually relevant to this question, but I am fully prepared to be humbled on that point.

@obi1kenobi
Copy link
Member

I agree that macro imports feel like the best effort-to-outcome ratio and lowest risk option here. Macros are already special anyway, and that's where our primary motivating example is too.

I think it's a good idea to start there, and then consider expanding to other namespaces based on what we learn from the macros case.

@petrochenkov
Copy link
Contributor

My understanding of how macros work and resolve is that their import/export rules are, in fact, special1.
Because they need to be resolved before you can expand code, and they have their own namespace, to begin with, plus the special prelude...

In imports, I don't think they are that special, all imports need to be resolved before you can expand code, because use a::b; can e.g. resolve to a module used to call a macro b::mac!(), and similar.

@traviscross
Copy link
Contributor

Thanks to @obi1kenobi for the detailed analysis. I'm going to leave some thoughts and questions as I work through it.

Strongly agree that the specific reexport chain is important. Consider the following:

mod inner {
    mod defn {
        pub struct Example;
    }
    #[doc(hidden)]
    mod nested {
       pub use super::defn::Example;
    }
    // This PR would allow this:
    #[deprecated]
    pub use defn::Example;
    pub use nested::Example;
}

Consider the following. Would you say it exhibits the same set of issues?

mod inner {
    mod defn {
        pub struct Example;
    }
    #[doc(hidden)]
    mod nested {
        pub use super::defn::Example;
    }
    mod dummy {
        #[deprecated]
        pub use super::defn::*;
        pub use super::nested::*;
    }
    pub use dummy::Example;
}

@traviscross
Copy link
Contributor

If we choose to allow duplicating more items than just macros, we have to grapple with "when is a type alias the same as the underlying type." This is a nightmare of a question.

For example:

pub mod a {
    pub struct Example<T>(T);
}
mod b {
    pub type Alias<T = i64> = super::a::Example<T>;   
}
pub use a::Example as Reexport;
// Should this be allowed?
pub use b::Alias as Reexport;

I'd expect we'd reject this, as we reject:

pub mod a {
    pub struct Example<T>(T);
}
mod b {
    pub type Alias<T = i64> = super::a::Example<T>;
}
mod a_reexport {
    pub use super::a::Example as Reexport;
}
mod b_reexport {
    pub use super::b::Alias as Reexport;
}
mod dummy {
    pub use super::a_reexport::*;
    pub use super::b_reexport::*;
}
pub use dummy::Reexport; //~ ERROR ambiguous name

Thoughts?

@traviscross
Copy link
Contributor

traviscross commented May 29, 2025

The other alternative for solving this problem, which might be a good idea regardless, is to provide some explicit syntax to control from which namespace an identifier is imported.

Doing this is possible today, but only with something that looks a bit absurd:

mod hack {
    #[expect(unused)]
    mod hack {
        // Create a macro namespace mask.
        macro_rules! Hack { () => {} }
        use Hack as Serialize;
        // Create a value namespace mask.
        #[expect(non_upper_case_globals)]
        const Serialize: () = ();
        // Import identifiers other than those masked out.
        pub(crate) use serde::*;
    }
    // Import only the type namespace `Serialize`.
    pub(crate) use hack::Serialize;
}
use hack::Serialize;

Of course, the workaround for this particular case -- that is, to import both serde_derive::Serialize and serde::Serialize without incurring breakage if the derive feature is set, and without glob importing into your code as a whole -- only requires:

mod hack {
    pub(crate) use serde::*;
    pub(crate) use serde_derive::*;
}
use hack::Serialize;

@obi1kenobi
Copy link
Member

Consider the following. Would you say it exhibits the same set of issues?

mod inner {
    mod defn {
        pub struct Example;
    }
    #[doc(hidden)]
    mod nested {
        pub use super::defn::Example;
    }
    mod dummy {
        #[deprecated]
        pub use super::defn::*;
        pub use super::nested::*;
    }
    pub use dummy::Example;
}

Yes, it has the same set of issues. So I'd frame my comment as "let's avoid making the existing problem worse."

Arguably this pattern should per se trigger a clippy or rustc lint, since this code is not setting up downstream users for success.

@obi1kenobi
Copy link
Member

I'd expect we'd reject this, as we reject:

pub mod a {
    pub struct Example<T>(T);
}
mod b {
    pub type Alias<T = i64> = super::a::Example<T>;
}
mod a_reexport {
    pub use super::a::Example as Reexport;
}
mod b_reexport {
    pub use super::b::Alias as Reexport;
}
mod dummy {
    pub use super::a_reexport::*;
    pub use super::b_reexport::*;
}
pub use dummy::Reexport; //~ ERROR ambiguous name

Thoughts?

IMHO it comes down to the basis upon which we reject it.

For example, today we currently also reject the following:

pub mod a {
    pub struct Example;
}
mod b {
    pub type Alias = super::a::Example;
}
mod a_reexport {
    pub use super::a::Example as Reexport;
}
mod b_reexport {
    pub use super::b::Alias as Reexport;
}
mod dummy {
    pub use super::a_reexport::*;
    pub use super::b_reexport::*;
}
pub use dummy::Reexport; //~ ERROR ambiguous name

In this case, the pub type is practically just a re-export of the original type. I believe the ambiguous re-export is the only way I can think of to observe the difference between the pub type and the original item. IIRC there was also a proposal to make this case completely equivalent to a re-export, in which case perhaps this pattern would become allowed in the future.

If so, then where do we draw the line for the "is duplicating allowed" question? For example, would we only allow duplicating if no generics are involved, to avoid the generic arg default value issue? That'd be quite strange. But other options seem even more problematic.

I think the space of possible interactions here is large, but unfortunately necessary to explore thoroughly before I'd feel confident in a path forward.

@traviscross
Copy link
Contributor

In this case, the pub type is practically just a re-export of the original type.

Probably I don't think of a type alias as simply a reexport, and I anticipate we'll be moving further in the direction of distinguishing these. See, e.g.:

In this model, then, it doesn't surprise me that the type alias can be distinguished from its definition, and I don't think we'd ever want to allow your example or mine to compile.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. I-lang-nominated Nominated for discussion during a lang team meeting. needs-fcp This change is insta-stable, or significant enough to need a team FCP to proceed. P-lang-drag-2 Lang team prioritization drag level 2.https://rust-lang.zulipchat.com/#narrow/channel/410516-t-lang. proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. S-waiting-on-documentation Status: Waiting on approved PRs to documentation before merging S-waiting-on-team Status: Awaiting decision from the relevant subteam (see the T-<team> label). T-lang Relevant to the language team
Projects
None yet
Development

Successfully merging this pull request may close these issues.