Allow mod
path to be directory containing module source (#2238)
This commit is contained in:
parent
d6669e0b97
commit
458805e283
@ -3204,7 +3204,10 @@ mod foo 'PATH'
|
|||||||
|
|
||||||
Which loads the module's source file from `PATH`, instead of from the usual
|
Which loads the module's source file from `PATH`, instead of from the usual
|
||||||
locations. A leading `~/` in `PATH` is replaced with the current user's home
|
locations. A leading `~/` in `PATH` is replaced with the current user's home
|
||||||
directory.
|
directory. `PATH` may point to the module source file itself, or to a directory
|
||||||
|
containing the module source file with the name `mod.just`, `justfile`, or
|
||||||
|
`.justfile`. In the latter two cases, the module file may have any
|
||||||
|
capitalization.
|
||||||
|
|
||||||
Environment files are only loaded for the root justfile, and loaded environment
|
Environment files are only loaded for the root justfile, and loaded environment
|
||||||
variables are available in submodules. Settings in submodules that affect
|
variables are available in submodules. Settings in submodules that affect
|
||||||
|
191
src/compiler.rs
191
src/compiler.rs
@ -43,17 +43,12 @@ impl Compiler {
|
|||||||
} => {
|
} => {
|
||||||
let parent = current.path.parent().unwrap();
|
let parent = current.path.parent().unwrap();
|
||||||
|
|
||||||
let import = if let Some(relative) = relative {
|
let relative = relative
|
||||||
let path = parent.join(Self::expand_tilde(&relative.cooked)?);
|
.as_ref()
|
||||||
|
.map(|relative| Self::expand_tilde(&relative.cooked))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
if path.is_file() {
|
let import = Self::find_module_file(parent, *name, relative.as_deref())?;
|
||||||
Some(path)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Self::find_module_file(parent, *name)?
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(import) = import {
|
if let Some(import) = import {
|
||||||
if current.file_path.contains(&import) {
|
if current.file_path.contains(&import) {
|
||||||
@ -111,19 +106,63 @@ impl Compiler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_module_file<'src>(parent: &Path, module: Name<'src>) -> RunResult<'src, Option<PathBuf>> {
|
fn find_module_file<'src>(
|
||||||
let mut candidates = vec![format!("{module}.just"), format!("{module}/mod.just")]
|
parent: &Path,
|
||||||
.into_iter()
|
module: Name<'src>,
|
||||||
.filter(|path| parent.join(path).is_file())
|
path: Option<&Path>,
|
||||||
.collect::<Vec<String>>();
|
) -> RunResult<'src, Option<PathBuf>> {
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
|
||||||
let directory = parent.join(module.lexeme());
|
if let Some(path) = path {
|
||||||
|
let full = parent.join(path);
|
||||||
|
|
||||||
if directory.exists() {
|
if full.is_file() {
|
||||||
let entries = fs::read_dir(&directory).map_err(|io_error| SearchError::Io {
|
return Ok(Some(full));
|
||||||
io_error,
|
}
|
||||||
directory: directory.clone(),
|
|
||||||
})?;
|
candidates.push((path.join("mod.just"), true));
|
||||||
|
|
||||||
|
for name in search::JUSTFILE_NAMES {
|
||||||
|
candidates.push((path.join(name), false));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
candidates.push((format!("{module}.just").into(), true));
|
||||||
|
candidates.push((format!("{module}/mod.just").into(), true));
|
||||||
|
|
||||||
|
for name in search::JUSTFILE_NAMES {
|
||||||
|
candidates.push((format!("{module}/{name}").into(), false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut grouped = BTreeMap::<PathBuf, Vec<(PathBuf, bool)>>::new();
|
||||||
|
|
||||||
|
for (candidate, case_sensitive) in candidates {
|
||||||
|
let candidate = parent.join(candidate).lexiclean();
|
||||||
|
grouped
|
||||||
|
.entry(candidate.parent().unwrap().into())
|
||||||
|
.or_default()
|
||||||
|
.push((candidate, case_sensitive));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut found = Vec::new();
|
||||||
|
|
||||||
|
for (directory, candidates) in grouped {
|
||||||
|
let entries = match fs::read_dir(&directory) {
|
||||||
|
Ok(entries) => entries,
|
||||||
|
Err(io_error) => {
|
||||||
|
if io_error.kind() == io::ErrorKind::NotFound {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(
|
||||||
|
SearchError::Io {
|
||||||
|
io_error,
|
||||||
|
directory,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let entry = entry.map_err(|io_error| SearchError::Io {
|
let entry = entry.map_err(|io_error| SearchError::Io {
|
||||||
@ -132,22 +171,34 @@ impl Compiler {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
if let Some(name) = entry.file_name().to_str() {
|
if let Some(name) = entry.file_name().to_str() {
|
||||||
for justfile_name in search::JUSTFILE_NAMES {
|
for (candidate, case_sensitive) in &candidates {
|
||||||
if name.eq_ignore_ascii_case(justfile_name) {
|
let candidate_name = candidate.file_name().unwrap().to_str().unwrap();
|
||||||
candidates.push(format!("{module}/{name}"));
|
|
||||||
|
let eq = if *case_sensitive {
|
||||||
|
name == candidate_name
|
||||||
|
} else {
|
||||||
|
name.eq_ignore_ascii_case(candidate_name)
|
||||||
|
};
|
||||||
|
|
||||||
|
if eq {
|
||||||
|
found.push(candidate.parent().unwrap().join(name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match candidates.as_slice() {
|
if found.len() > 1 {
|
||||||
[] => Ok(None),
|
found.sort();
|
||||||
[file] => Ok(Some(parent.join(file).lexiclean())),
|
Err(Error::AmbiguousModuleFile {
|
||||||
found => Err(Error::AmbiguousModuleFile {
|
found: found
|
||||||
found: found.into(),
|
.into_iter()
|
||||||
|
.map(|found| found.strip_prefix(parent).unwrap().into())
|
||||||
|
.collect(),
|
||||||
module,
|
module,
|
||||||
}),
|
})
|
||||||
|
} else {
|
||||||
|
Ok(found.into_iter().next())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,4 +293,84 @@ recipe_b: recipe_c
|
|||||||
import == tmp.path().join("justfile").lexiclean()
|
import == tmp.path().join("justfile").lexiclean()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_module_file() {
|
||||||
|
#[track_caller]
|
||||||
|
fn case(path: Option<&str>, files: &[&str], expected: Result<Option<&str>, &[&str]>) {
|
||||||
|
let module = Name {
|
||||||
|
token: Token {
|
||||||
|
column: 0,
|
||||||
|
kind: TokenKind::Identifier,
|
||||||
|
length: 3,
|
||||||
|
line: 0,
|
||||||
|
offset: 0,
|
||||||
|
path: Path::new(""),
|
||||||
|
src: "foo",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
for file in files {
|
||||||
|
if let Some(parent) = Path::new(file).parent() {
|
||||||
|
fs::create_dir_all(tempdir.path().join(parent)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write(tempdir.path().join(file), "").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let actual = Compiler::find_module_file(tempdir.path(), module, path.map(Path::new));
|
||||||
|
|
||||||
|
match expected {
|
||||||
|
Err(expected) => match actual.unwrap_err() {
|
||||||
|
Error::AmbiguousModuleFile { found, .. } => {
|
||||||
|
assert_eq!(
|
||||||
|
found,
|
||||||
|
expected
|
||||||
|
.iter()
|
||||||
|
.map(|expected| expected.replace('/', std::path::MAIN_SEPARATOR_STR).into())
|
||||||
|
.collect::<Vec<PathBuf>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => panic!("unexpected error"),
|
||||||
|
},
|
||||||
|
Ok(Some(expected)) => assert_eq!(
|
||||||
|
actual.unwrap().unwrap(),
|
||||||
|
tempdir
|
||||||
|
.path()
|
||||||
|
.join(expected.replace('/', std::path::MAIN_SEPARATOR_STR))
|
||||||
|
),
|
||||||
|
Ok(None) => assert_eq!(actual.unwrap(), None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case(None, &["foo.just"], Ok(Some("foo.just")));
|
||||||
|
case(None, &["FOO.just"], Ok(None));
|
||||||
|
case(None, &["foo/mod.just"], Ok(Some("foo/mod.just")));
|
||||||
|
case(None, &["foo/MOD.just"], Ok(None));
|
||||||
|
case(None, &["foo/justfile"], Ok(Some("foo/justfile")));
|
||||||
|
case(None, &["foo/JUSTFILE"], Ok(Some("foo/JUSTFILE")));
|
||||||
|
case(None, &["foo/.justfile"], Ok(Some("foo/.justfile")));
|
||||||
|
case(None, &["foo/.JUSTFILE"], Ok(Some("foo/.JUSTFILE")));
|
||||||
|
case(
|
||||||
|
None,
|
||||||
|
&["foo/.justfile", "foo/justfile"],
|
||||||
|
Err(&["foo/.justfile", "foo/justfile"]),
|
||||||
|
);
|
||||||
|
case(None, &["foo/JUSTFILE"], Ok(Some("foo/JUSTFILE")));
|
||||||
|
|
||||||
|
case(Some("bar"), &["bar"], Ok(Some("bar")));
|
||||||
|
case(Some("bar"), &["bar/mod.just"], Ok(Some("bar/mod.just")));
|
||||||
|
case(Some("bar"), &["bar/justfile"], Ok(Some("bar/justfile")));
|
||||||
|
case(Some("bar"), &["bar/JUSTFILE"], Ok(Some("bar/JUSTFILE")));
|
||||||
|
case(Some("bar"), &["bar/.justfile"], Ok(Some("bar/.justfile")));
|
||||||
|
case(Some("bar"), &["bar/.JUSTFILE"], Ok(Some("bar/.JUSTFILE")));
|
||||||
|
|
||||||
|
case(
|
||||||
|
Some("bar"),
|
||||||
|
&["bar/justfile", "bar/mod.just"],
|
||||||
|
Err(&["bar/justfile", "bar/mod.just"]),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ use super::*;
|
|||||||
pub(crate) enum Error<'src> {
|
pub(crate) enum Error<'src> {
|
||||||
AmbiguousModuleFile {
|
AmbiguousModuleFile {
|
||||||
module: Name<'src>,
|
module: Name<'src>,
|
||||||
found: Vec<String>,
|
found: Vec<PathBuf>,
|
||||||
},
|
},
|
||||||
ArgumentCountMismatch {
|
ArgumentCountMismatch {
|
||||||
recipe: &'src str,
|
recipe: &'src str,
|
||||||
@ -262,7 +262,7 @@ impl<'src> ColorDisplay for Error<'src> {
|
|||||||
AmbiguousModuleFile { module, found } =>
|
AmbiguousModuleFile { module, found } =>
|
||||||
write!(f,
|
write!(f,
|
||||||
"Found multiple source files for module `{module}`: {}",
|
"Found multiple source files for module `{module}`: {}",
|
||||||
List::and_ticked(found),
|
List::and_ticked(found.iter().map(|path| path.display())),
|
||||||
)?,
|
)?,
|
||||||
ArgumentCountMismatch { recipe, found, min, max, .. } => {
|
ArgumentCountMismatch { recipe, found, min, max, .. } => {
|
||||||
let count = Count("argument", *found);
|
let count = Count("argument", *found);
|
||||||
|
@ -19,7 +19,7 @@ pub(crate) use {
|
|||||||
fs,
|
fs,
|
||||||
io::Write,
|
io::Write,
|
||||||
iter,
|
iter,
|
||||||
path::{Path, PathBuf, MAIN_SEPARATOR},
|
path::{Path, PathBuf, MAIN_SEPARATOR, MAIN_SEPARATOR_STR},
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
str,
|
str,
|
||||||
},
|
},
|
||||||
|
@ -439,12 +439,13 @@ fn modules_require_unambiguous_file() {
|
|||||||
.status(EXIT_FAILURE)
|
.status(EXIT_FAILURE)
|
||||||
.stderr(
|
.stderr(
|
||||||
"
|
"
|
||||||
error: Found multiple source files for module `foo`: `foo.just` and `foo/justfile`
|
error: Found multiple source files for module `foo`: `foo/justfile` and `foo.just`
|
||||||
——▶ justfile:1:5
|
——▶ justfile:1:5
|
||||||
│
|
│
|
||||||
1 │ mod foo
|
1 │ mod foo
|
||||||
│ ^^^
|
│ ^^^
|
||||||
",
|
"
|
||||||
|
.replace('/', MAIN_SEPARATOR_STR),
|
||||||
)
|
)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
@ -564,6 +565,23 @@ fn modules_may_specify_path() {
|
|||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modules_may_specify_path_to_directory() {
|
||||||
|
Test::new()
|
||||||
|
.write("commands/bar/mod.just", "foo:\n @echo FOO")
|
||||||
|
.justfile(
|
||||||
|
"
|
||||||
|
mod foo 'commands/bar'
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.test_round_trip(false)
|
||||||
|
.arg("--unstable")
|
||||||
|
.arg("foo")
|
||||||
|
.arg("foo")
|
||||||
|
.stdout("FOO\n")
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn modules_with_paths_are_dumped_correctly() {
|
fn modules_with_paths_are_dumped_correctly() {
|
||||||
Test::new()
|
Test::new()
|
||||||
|
Loading…
Reference in New Issue
Block a user