Skip to content

Experiment proposal: #[discourage_direct_import] attribute #373

Description

@nik-rev

Experimental feature gate: #![feature(discourage_direct_import)]

Previous form of proposal (for archive purposes only)

I propose to add a new lint for making imports in codebases more consistent, which fires when items are imported inconsistently.

For example, many items in the std::fs, std::io, std::iter, std::env and other modules are conventionally referred to via the parent path:

use std::fs;
use std::fmt;
use std::io;

fs::read("Cargo.toml")?;

impl fmt::Write for Struct { ... }

fn save_to_disk(file: &str) -> io::Result<()> { ... }

It is discouraged to import those items directly:

use std::fs::read;
use std::fmt::Write;
use std::io::Result; // shadows std::result::Result, causing confusion

read("Cargo.toml")?;

// is this io::Write or fmt::Write?
impl Write for Struct { ... }

// shadows std::result::Result, causing confusion
fn save_to_disk(file: &str) -> Result<()> {

To enforce this style, there is a new attribute #[import_style]. This attribute configures the new wrong_import_style_via_super warn-by-default lint.

The #[import_style] attribute tells how others should import your item.

For example, we can annotate fs::read with it:

[!NOTE]

All syntax in this proposal is just placeholder.

// std::fs
#[import_style(via = super)]
fn read() {}

When someone imports read directly, the lint wrong_import_style_via_super fires:

use std::fs::read;
//           ^^^^ do not directly import
//                instead, `use std::fs` then refer as `fs::read`

The basic MVP would be to implement #[import_style(via = super)], with no further configuration.

Extensions

The rest of this proposal goes into how this feature can be further extended to cover other common patterns.

Specifically, the use-cases the extensions support are:

  • Items imported directly

    use rustc_middle::ty::Ty commonly is referred to directly as Ty.

    We should allow linting other ways of referring to Ty, for consumer
    crates, the current crate, or a specific module.

  • Types imported , but the module name should be renamed. Like use rustc_hir as hir, then refer as hir::Ty. Other ways of referring are linted.

  • Override import style for external paths

  • Ignore the import style for external path

The proposal provides a bird's eye view of the feature, there are many details left out that can be figured out during the implementation.

Feature: Direct import

We can require that the item is imported directly. Let's say we want to require that rustc_middle::ty::Ty is always used as Ty, with no path segments:

// rustc_middle::ty

#[import_style(via = self)]
struct Ty;

Then not using the item directly triggers the lint wrong_import_style_via_self:

use rustc_middle::ty;
// +                ::Ty
//                  import `Ty` directly

fn f(_: ty::Ty) {}
//      ^^^^ wrong import style, `Ty` should be
//           imported directly.

The following is required:

use rustc_middle::ty::Ty;

fn f(_: Ty) {}

The wrong_import_style_via_self lint is allow-by-default
to prevent libraries from polluting the user's scope.

Feature: Renaming

The import_style field also accepts an as key, which requires the item to be renamed when it is imported directly.

For example:

// syn::parse

#[import_style(via = self, as = SynParse)]
trait Parse {

All of the following usage is wrong:

// refer as `syn::parse::Parse`
use syn::parse; // then refer as `parse::Parse`
use syn::parse::Parse; // then refer as Parse

You must import Parse directly, then rename it:

use syn::parse::Parse as SynParse

If via = self is omitted, then Parse needs to be renamed only when imported directly.

This cannot be used with via = super.

Feature: Configure external items

We have an issue with paths that are too awkward:

impl syn::parse::Parse for RenameRule {
  • syn::parse::Parse is too long to type, and contains duplication in "parse".
  • use syn::parse, then refer as parse::Parse looks odd + not clear that it's from syn
  • use syn::parse::Parse then refer as Parse has large potential to conflict with other types, it's also not clear that the type is from syn

Given that, one may reasonably require the following style in their codebase:

use syn::parse::Parse as SynParse;

impl SynParse for RenameRule {

To enforce this, the syn crate could add import_style attribute to Parse:

#[import_style(via = self, as = SynParse)]
trait Parse {

But they might not do that, and we still want to enforce that style in our codebase.

To accomodate this, we can configure the import style for other items:

#![import_style(for = syn::parse::Parse, via = self, as = SynParse)]

This works much like a #[warn]:

  • On functions, sets the import style within the body
  • On modules, sets the import style for the item to all sub-items recursively
  • On the crate root, sets the import style for the whole crate

Ignoring

It may also be desireable to ignore the import style of specific items, which can be done with ::ignore:

#![import_style::ignore(std::fs::read)]

That accepts a list of paths. For any path that is ignored, all sub-items in that path is also ignored.

This ignores all #[import_style] attributes on items in std::io and std::fs:

#![import_style::ignore(std::fs, std::io)]

In which case this no longer triggers the wrong_import_style_via_super lint:

use std::fs::read;

read("Cargo.toml"); // OK

I propose to add a new lint for making imports in codebases more consistent, which fires when certain items are imported directly.

For example, many items in the std::fs, std::io, std::iter, std::env and other modules are conventionally referred to via the parent path:

use std::fs;
use std::fmt;
use std::io;

fs::read("Cargo.toml")?;

impl fmt::Write for Struct { ... }

fn save_to_disk(file: &str) -> io::Result<()> { ... }

It is discouraged to import those items directly:

use std::fs::read;
use std::fmt::Write;
use std::io::Result; // shadows std::result::Result, causing confusion

read("Cargo.toml")?;

// is this io::Write or fmt::Write?
impl Write for Struct { ... }

// shadows std::result::Result, causing confusion
fn save_to_disk(file: &str) -> Result<()> {

To enforce this style, there is a new attribute #[discourage_direct_import]. This attribute configures the new discouraged_direct_imports warn-by-default lint.

The #[import_style] attribute tells how others should import your item.

For example, we can annotate fs::read with it:

// std::fs
#[discourage_direct_import]
fn read() {}

When someone imports read directly, the lint wrong_import_style_via_super fires:

use std::fs::read;
//           ^^^^ do not directly import
//                instead, `use std::fs` then refer as `fs::read`

Another example use-case: One of my code bases has 80 types named Request, each in their individual modules. This API is meant to be used via a full type to Request, you'd never import it directly.

Contacts

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions