Motivation
I wasn't a big fan of the change in RFC 2126 that made mod.rs
files optional (allowing foo.rs
and foo/
) as I think the problem it aimed to solve was relatively trivial compared to the new problems it created (especially now that IDE plugins like IntelliJ-Rust have a "convert module to directory" intention).
That is, mainly, jumping back and forth between a module file and its submodules (which is pretty common since they should contain related code) now usually means scanning the list of files in the parent directory (since nearly every file manager and every IDE sorts folders above files). Because of this same issue, you cannot tell at a glance which modules have submodules or not as you have to perform a visual diff between the files and folders in a given directory.
I'm also just a curmudgeon who avoids change when I can (heck, I still use old Reddit).
However, I can get used to the new module style; the real issue is that while RFC 2126 promised lints to cover the other changes, this part seems to have been left by the wayside, and so there's no way to enforce consistency by default. This results in projects having to enforce this either procedurally in code review or by cobbling together a script to enforce their chosen module style in CI.
For proprietary projects in a rapid prototyping phase, this kind of thing is easily forgotten and so you can sometimes end up with messy projects that mix and match both mod.rs
files and RFC 2126's style (which I will hereafter refer to as "2015 edition style" and "2018 edition style", respectively; while the new style didn't require 2018 edition, the two are strongly associated), which is honestly the worst of both worlds.
Proposal
Henceforth, I'd like to propose three related lints for inclusion directly in rustc
(as Clippy is easy to forget to run and thus runs into the same procedural issues in projects that are moving quickly). The first two are pretty straightforward:
-
edition_2015_module_style
, allowed by default, which lints againstmod.rs
files in the crate/current module tree -
edition_2018_module_style
, allowed by default, which lints against having bothfoo.rs
andfoo/
in same
The names are open for bikeshedding, although I chose these names because the way lints work, users will want to #![deny(edition_2015_module_style)]
at the crate root to enforce the 2018 module style and vice-versa. These lints will be cargo fix
able.
The third lint, mixed_module_style
, is warn by default and fires if the crate mixes and matches both 2015 and 2018 styles; it will not be cargo fix
able but instead recommend in help messages that the user choose either #![deny(edition_2015_module_style)]
or #![deny(edition_2018_module_style)]
to place at the crate root.
Alternatives
Single-Lint Solution
For simplicity of implementation, I figured the mixed_module_style
lint could just visit the whole module tree once, determining the style for each module and then generating a lint warning on an edge trigger (e.g. the last module it saw used 2018 style but the current module uses 2015 style), however that might produce false-positives from the user's perspective as it would generate two warnings if it saw three modules with 2018, 2015, 2018
styles even though the middle module is the only odd one out.
Instead, the mixed_module_style
lint could pull double duty and visit the whole crate up front to determine the style used by the majority of the modules, and then generate lint warnings for any that don't follow the majority. This of course means visiting the whole crate twice, but since it's only looking at modules this is likely a non-issue for compilation times.
Then, mixed_module_style
could be cargo fix
able and the user wouldn't have to manually pick a style to enforce.
The only issue is if there's a tie; the choice here should probably not be arbitrary but it's also not obvious what it should be; maybe whichever style it saw first, or maybe the edition the crate was compiled with.
Rustfmt Formatting Rule
This could instead be an option in rustfmt.toml
:
module_style = "<2015 | 2018>"
This is also something most users would probably want to configure once workspace-wide rather than at the root of every crate in their project, which may be another reason to put this in rustfmt
instead.
However, I don't think rustfmt
currently supports any formatting rules that require moving/renaming files so this may require some hacking to implement, compared to the proposed lints which I already have a vague mental plan for implementing. It also somewhat runs into the same procedural issues as mentioned above, though it has the benefit of IDE support for being run automatically on save which wouldn't require any user intervention.
Although an issue with rustfmt
doing this automatically is that it may not check the renamed/moved file into version control which means the user may accidentally commit broken code without intending to. I think though that this could be fixed/papered-over in, e.g. IntelliJRust as it just has to be aware that rustfmt
can move files around now and to automatically check-in any that got moved with IDEA's Git integration.
Meanwhile, a solution like a #[deny(...)]
'd lint would force user intervention and also remind them which module style the project is using so that they can remember to adhere to it.
Unresolved Questions
Is there any good reason to allow configuring these lints anywhere but the crate root? It's kind of antithetical to this proposal to choose one style for one module subtree and a different style for another. (Just thought of one: generated code.)