///|
/// Describes errors raised before traversal starts.
pub(all) suberror WalkError {
NotFound(String)
NotDirectory(String)
} derive(Debug)
///|
pub impl Show for WalkError with fn output(error, logger) {
match error {
NotFound(path) => {
logger.write_string("NotFound(")
logger.write(path)
logger.write_string(")")
}
NotDirectory(path) => {
logger.write_string("NotDirectory(")
logger.write(path)
logger.write_string(")")
}
}
}
///|
/// Walks a directory tree in deterministic depth-first order.
///
/// Children are sorted lexicographically by default so repeated runs produce
/// the same output. Set `include_root=false` to skip the root entry and
/// `include_dirs=false` to emit only files. `max_depth=0` keeps traversal at
/// the root level.
///
/// # Parameters
///
/// - `root`: The directory to traverse.
/// - `include_root`: Whether the returned array should include the root itself.
/// - `include_dirs`: Whether directories should be included in the returned array.
/// - `max_depth`: The maximum depth to descend from `root`.
/// - `sort`: Whether to sort each directory's children before traversal.
///
/// # Errors
///
/// - Raises `WalkError::NotFound(root)` when `root` does not exist.
/// - Raises `WalkError::NotDirectory(root)` when `root` is not a directory.
///
/// # Example
/// ```mbt check
/// test "walk example" {
/// let entries = @walkdir.walk("testdata/walk_fixture", max_depth=1)
/// assert_eq(entries.length(), 4)
/// }
/// ```
pub fn walk(
root : String,
include_root? : Bool = true,
include_dirs? : Bool = true,
max_depth? : Int,
sort? : Bool = true,
) -> Array[DirEntry] raise {
validate_root(root)
let entries : Array[DirEntry] = []
if include_root {
entries.push(make_entry(root, 0, Dir))
}
walk_dir(root, 0, include_dirs~, max_depth, sort~, entries)
entries
}
///|
/// Walks a directory tree and returns only file paths.
///
/// This is a convenience wrapper around `walk(..., include_dirs=false,
/// include_root=false)` for file-oriented use cases such as indexing or
/// filtering.
///
/// # Parameters
///
/// - `root`: The directory to traverse.
/// - `max_depth`: The maximum depth to descend from `root`.
/// - `sort`: Whether to sort each directory's children before traversal.
///
/// # Errors
///
/// Raises the same errors as `walk`.
///
/// # Example
/// ```mbt check
/// test "walk_files example" {
/// let files = @walkdir.walk_files("testdata/walk_fixture")
/// assert_eq(files.length(), 4)
/// }
/// ```
pub fn walk_files(
root : String,
max_depth? : Int,
sort? : Bool = true,
) -> Array[String] raise {
let entries = match max_depth {
Some(depth) =>
walk(root, include_root=false, include_dirs=false, max_depth=depth, sort~)
None => walk(root, include_root=false, include_dirs=false, sort~)
}
entries.map(entry => entry.path)
}
///|
/// Validates that traversal starts from an existing directory.
fn validate_root(root : String) -> Unit raise {
if !@fs.path_exists(root) {
raise WalkError::NotFound(root)
}
if !@fs.is_dir(root) {
raise WalkError::NotDirectory(root)
}
}
///|
/// Recursively collects entries under the current directory.
fn walk_dir(
current : String,
current_depth : Int,
include_dirs~ : Bool,
max_depth : Int?,
sort~ : Bool,
entries : Array[DirEntry],
) -> Unit raise {
guard can_descend(current_depth, max_depth) else { () }
let children = @fs.read_dir(current)
if sort {
children.sort_by((lhs, rhs) => lhs.lexical_compare(rhs))
}
let base : @path.Path = current
for name in children {
let child : @path.Path = name
let child_path = "\{base.join(child)}"
let child_depth = current_depth + 1
if @fs.is_dir(child_path) {
if include_dirs {
entries.push(make_entry(child_path, child_depth, Dir))
}
walk_dir(
child_path,
child_depth,
include_dirs~,
max_depth,
sort~,
entries,
)
} else {
entries.push(make_entry(child_path, child_depth, File))
}
}
}
///|
/// Decides whether traversal may continue below the current depth.
fn can_descend(current_depth : Int, max_depth : Int?) -> Bool {
match max_depth {
Some(limit) => current_depth < limit
None => true
}
}
///|
/// Builds a `DirEntry` value from raw traversal data.
fn make_entry(path : String, depth : Int, kind : EntryKind) -> DirEntry {
{ path, depth, kind }
}