Skip to content

← Modules, Visibility, Testing and Docs step 11 of 22

Medium Framework

Module file layout: foo.rs, foo/mod.rs, and #[path]

This is the part of the module system most beginners think is the module system — and it is the one thing this site cannot demonstrate directly, because your submission is a single file with no filesystem behind it. So you implement the resolution rules instead, and the article has to be unusually precise.

pub fn module_file(entry: &str, module_path: Vec<String>, style: &str) -> String
pub fn path_attr_file(containing_file: &str, inline_mods: Vec<String>, attr_path: &str) -> String

mod foo; and where the body comes from

mod foo; — with a semicolon, no block — tells the compiler “the body of this module is in another file”. There are exactly two places it will look, and it is an error for both to exist:

src/foo.rs          <- the modern style
src/foo/mod.rs      <- the older style

Which directory it looks in is the interesting part, and the rule composes. A submodule’s file lives under a directory named after its parent module:

src/lib.rs      mod util;                  ->  src/util.rs
src/util.rs     mod config;                ->  src/util/config.rs

Yes, src/util.rs and src/util/ coexist. That looks wrong to newcomers and it is correct: the file holds the module, the directory holds its children. With mod.rs style it is one directory deeper the whole way:

src/lib.rs      mod util;                  ->  src/util/mod.rs
                mod util::config;          ->  src/util/config/mod.rs

module_file implements exactly this: style is "self_named" or "mod_rs", module_path is the chain of module names from the entry file down, and an empty chain returns the entry file itself.

#[path], and the most obscure rule in the modules chapter

#[path = "…"] overrides the search. Its base directory depends on what kind of file the attribute is written in:

  • mod-rs files are the ones that own their directory: lib.rs, main.rs, and any mod.rs.
  • non-mod-rs files are all other module files — util.rs, platform.rs.

And then:

where the #[path] sits resolved relative to
not inside any inline mod block the directory of the containing file
inside inline blocks, in a mod-rs file that directory, then the inline module names as directories
inside inline blocks, in a non-mod-rs file that directory, then a directory named after the file, then the inline module names

Concretely:

src/a/mod.rs   mod inline { #[path = "other.rs"] mod x; }  ->  src/a/inline/other.rs
src/a/b.rs     mod inline { #[path = "other.rs"] mod x; }  ->  src/a/b/inline/other.rs
src/a/b.rs     #[path = "other.rs"] mod x;                 ->  src/a/other.rs

The extra b/ in the second line is the rule almost nobody knows, and it is how platform-specific trees get built:

#[cfg(unix)]    #[path = "unix.rs"]    mod sys;
#[cfg(windows)] #[path = "windows.rs"] mod sys;

Two clippy lints that enforce opposite conventions

  • clippy::mod_module_files — “every module should be foo/mod.rs“.
  • clippy::self_named_module_files — “every module should be foo.rs“.

Both are restriction. Enabling both is always wrong; enabling neither is completely fine. This is the cleanest example in the whole lint set of what restriction means: a menu of team policies, not a quality standard. Pick one if your team cares, and then the lint stops the drift.

What does not work here

mod foo; with no body cannot work under this harness at all — there is no second file to find. Every module in a submission must be an inline block. That is a property of this site, not of Rust, and it is why this item is an oracle problem.