feat(apply): consolidate logic in --dry-run
This commit is contained in:
parent
289aa82ded
commit
f23a9b2653
11 changed files with 268 additions and 183 deletions
|
|
@ -1,6 +1,6 @@
|
|||
use super::{find_config_file, parse_config, type_check};
|
||||
use doot_core::state::{StateStore, SyncStatus};
|
||||
use doot_core::{Config, Deployer};
|
||||
use doot_core::{Config, DeployAction, Deployer};
|
||||
use doot_lang::ast::HookStage;
|
||||
use doot_lang::evaluator::{DotfileConfig, DotfilesPattern, DotfilesSource, HookConfig};
|
||||
use doot_lang::{DotfileConflict, Evaluator, validate_dotfile_targets};
|
||||
|
|
@ -121,6 +121,28 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
|
|||
if full_source.is_dir() {
|
||||
let changed_files = state.get_changed_files_in_dir(&full_source, &dotfile.target);
|
||||
|
||||
// Filter out excluded files before checking for changes
|
||||
let changed_files: Vec<_> = changed_files
|
||||
.into_iter()
|
||||
.filter(|(src, tgt, _)| {
|
||||
if dotfile
|
||||
.exclude_paths
|
||||
.iter()
|
||||
.any(|ex| tgt.starts_with(ex) || ex == tgt)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if dotfile
|
||||
.exclude_sources
|
||||
.iter()
|
||||
.any(|ex| src.strip_prefix(&full_source) == Ok(ex.as_path()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut has_real_conflicts = false;
|
||||
let mut has_changes = false;
|
||||
|
||||
|
|
@ -310,85 +332,15 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
|
|||
}
|
||||
}
|
||||
|
||||
// Dry-run: show what would be done and exit
|
||||
if dry_run {
|
||||
if deploy_set.is_empty() {
|
||||
println!("\n[dry-run] all dotfiles synced, nothing to deploy");
|
||||
} else {
|
||||
println!("\n[dry-run] would deploy:");
|
||||
for &idx in &deploy_set {
|
||||
let dotfile = &result.dotfiles[idx];
|
||||
println!(
|
||||
" {} -> {}",
|
||||
dotfile.source.display(),
|
||||
dotfile.target.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !result.packages.is_empty() {
|
||||
if let Some(manager) = doot_core::package::detect_package_manager() {
|
||||
let mut to_install = Vec::new();
|
||||
let mut already_installed = Vec::new();
|
||||
|
||||
for pkg in &result.packages {
|
||||
if let Some(ref name) = pkg.default {
|
||||
match manager.is_installed(name) {
|
||||
Ok(true) => already_installed.push(name.clone()),
|
||||
_ => to_install.push(name.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !already_installed.is_empty() {
|
||||
println!("\n[dry-run] packages already installed:");
|
||||
for pkg in &already_installed {
|
||||
println!(" {}", pkg);
|
||||
}
|
||||
}
|
||||
|
||||
if !to_install.is_empty() {
|
||||
println!("\n[dry-run] would install packages:");
|
||||
for pkg in &to_install {
|
||||
println!(" {}", pkg);
|
||||
}
|
||||
} else if already_installed.is_empty() {
|
||||
println!("\n[dry-run] no packages to install");
|
||||
}
|
||||
} else {
|
||||
println!("\n[dry-run] no supported package manager found");
|
||||
}
|
||||
}
|
||||
|
||||
// Show packages that would be pruned
|
||||
{
|
||||
let configured_names: std::collections::HashSet<String> = result
|
||||
.packages
|
||||
.iter()
|
||||
.filter_map(|p| p.default.clone())
|
||||
.collect();
|
||||
let state_for_prune = StateStore::new(&state_file);
|
||||
let to_prune: Vec<_> = state_for_prune
|
||||
.get_all_packages()
|
||||
.iter()
|
||||
.filter(|(name, _)| !configured_names.contains(*name))
|
||||
.collect();
|
||||
if !to_prune.is_empty() {
|
||||
println!("\n[dry-run] would uninstall removed packages:");
|
||||
for (name, _) in &to_prune {
|
||||
println!(" {}", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
let dry_prefix = if dry_run { "[dry-run] " } else { "" };
|
||||
|
||||
// Run before_deploy hooks
|
||||
if !dry_run {
|
||||
run_hooks(&result.hooks, HookStage::BeforeDeploy, &hook_env)?;
|
||||
}
|
||||
|
||||
if deploy_set.is_empty() {
|
||||
println!("\nNothing to deploy (all files synced).");
|
||||
println!("\n{}all dotfiles synced, nothing to deploy", dry_prefix);
|
||||
} else {
|
||||
// Filter parallel batches to only include items in deploy_set
|
||||
let filtered_batches: Vec<Vec<usize>> = validation
|
||||
|
|
@ -406,6 +358,7 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
|
|||
|
||||
let deployer = Deployer::new(config, result.sandbox);
|
||||
|
||||
let progress = if !dry_run {
|
||||
let pb = ProgressBar::new(deploy_set.len() as u64);
|
||||
pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
|
|
@ -413,15 +366,46 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
|
|||
.unwrap()
|
||||
.progress_chars("=>-"),
|
||||
);
|
||||
|
||||
pb.set_message("deploying dotfiles");
|
||||
Some(pb)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let deploy_result =
|
||||
deployer.deploy_batches(&result.dotfiles, &filtered_batches, Some(&pb))?;
|
||||
deployer.deploy_batches(&result.dotfiles, &filtered_batches, progress.as_ref())?;
|
||||
|
||||
if let Some(pb) = progress {
|
||||
pb.finish_with_message("done");
|
||||
}
|
||||
|
||||
if dry_run {
|
||||
// Dry-run: show what the conflict-check decided needs deploying
|
||||
println!("\n{}would deploy:", dry_prefix);
|
||||
for &idx in &deploy_set {
|
||||
let dotfile = &result.dotfiles[idx];
|
||||
println!(
|
||||
" {} -> {}",
|
||||
dotfile.source.display(),
|
||||
dotfile.target.display()
|
||||
);
|
||||
}
|
||||
|
||||
if !deploy_result.errors.is_empty() {
|
||||
println!("\n{}errors:", dry_prefix);
|
||||
for error in &deploy_result.errors {
|
||||
println!(" {} ({})", error.target.display(), error.error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let active: Vec<_> = deploy_result
|
||||
.deployed
|
||||
.iter()
|
||||
.filter(|d| !matches!(d.action, DeployAction::Unchanged))
|
||||
.collect();
|
||||
|
||||
println!("\ndeployment complete:");
|
||||
println!(" deployed: {}", deploy_result.deployed.len());
|
||||
println!(" deployed: {}", active.len());
|
||||
println!(" skipped: {}", deploy_result.skipped.len());
|
||||
println!(" errors: {}", deploy_result.errors.len());
|
||||
|
||||
|
|
@ -446,16 +430,20 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
|
|||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run after_deploy hooks
|
||||
if !dry_run {
|
||||
run_hooks(&result.hooks, HookStage::AfterDeploy, &hook_env)?;
|
||||
}
|
||||
|
||||
// Package handling
|
||||
if !result.packages.is_empty() {
|
||||
// Run before_package hooks
|
||||
if !dry_run {
|
||||
run_hooks(&result.hooks, HookStage::BeforePackage, &hook_env)?;
|
||||
}
|
||||
|
||||
if let Some(manager) = doot_core::package::detect_package_manager() {
|
||||
// Filter out already installed packages
|
||||
let mut to_install = Vec::new();
|
||||
let mut already_installed = Vec::new();
|
||||
|
||||
|
|
@ -469,6 +457,12 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
|
|||
}
|
||||
|
||||
if !already_installed.is_empty() {
|
||||
if dry_run {
|
||||
println!("\n{}packages already installed:", dry_prefix);
|
||||
for pkg in &already_installed {
|
||||
println!(" {}", pkg);
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
count = already_installed.len(),
|
||||
"packages already installed"
|
||||
|
|
@ -477,32 +471,42 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
|
|||
tracing::debug!(package = %pkg, "already installed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if to_install.is_empty() {
|
||||
if !dry_run {
|
||||
println!(
|
||||
"\nall {} packages already installed",
|
||||
already_installed.len()
|
||||
);
|
||||
}
|
||||
} else if dry_run {
|
||||
println!("\n{}would install packages:", dry_prefix);
|
||||
for pkg in &to_install {
|
||||
println!(" {}", pkg);
|
||||
}
|
||||
} else {
|
||||
println!("\ninstalling {} packages...", to_install.len());
|
||||
manager.install(&to_install)?;
|
||||
println!("installed {} packages", to_install.len());
|
||||
}
|
||||
|
||||
// Record all managed packages in state (both newly installed and already installed)
|
||||
if !dry_run {
|
||||
let mut state = StateStore::new(&state_file);
|
||||
let manager_name = manager.name();
|
||||
for pkg in to_install.iter().chain(already_installed.iter()) {
|
||||
state.record_package(pkg, manager_name);
|
||||
}
|
||||
state.save()?;
|
||||
}
|
||||
} else {
|
||||
println!("no supported package manager found");
|
||||
}
|
||||
|
||||
// Run after_package hooks
|
||||
if !dry_run {
|
||||
run_hooks(&result.hooks, HookStage::AfterPackage, &hook_env)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Prune packages removed from config
|
||||
{
|
||||
|
|
@ -521,6 +525,12 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
|
|||
.collect();
|
||||
|
||||
if !to_prune.is_empty() {
|
||||
if dry_run {
|
||||
println!("\n{}would uninstall removed packages:", dry_prefix);
|
||||
for (name, _) in &to_prune {
|
||||
println!(" {}", name);
|
||||
}
|
||||
} else {
|
||||
println!("\n{} package(s) removed from config:", to_prune.len());
|
||||
for (name, _) in &to_prune {
|
||||
println!(" {}", name);
|
||||
|
|
@ -553,6 +563,7 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
println!("\nfinished in {:.2?}", elapsed);
|
||||
|
|
@ -767,6 +778,7 @@ fn expand_dotfile_patterns(
|
|||
link_patterns: pattern.link_patterns.clone(),
|
||||
copy_patterns: pattern.copy_patterns.clone(),
|
||||
exclude_paths: vec![],
|
||||
exclude_sources: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -775,9 +787,11 @@ fn expand_dotfile_patterns(
|
|||
|
||||
/// Merges explicit dotfile blocks into glob-expanded entries.
|
||||
///
|
||||
/// Two merge cases:
|
||||
/// Three merge cases:
|
||||
/// 1. Same target: explicit replaces glob-expanded entry entirely.
|
||||
/// 2. File inside directory: adds the file's target to the directory entry's exclude_paths.
|
||||
/// 2. Target inside directory target: adds the file's target to exclude_paths.
|
||||
/// 3. Source inside directory source: adds the file's source to exclude_sources
|
||||
/// (handles cases where the explicit block targets a different location).
|
||||
fn merge_specializations(dotfiles: &mut Vec<DotfileConfig>, glob_count: usize) {
|
||||
let total = dotfiles.len();
|
||||
let explicit_end = total - glob_count;
|
||||
|
|
@ -801,7 +815,7 @@ fn merge_specializations(dotfiles: &mut Vec<DotfileConfig>, glob_count: usize) {
|
|||
}
|
||||
}
|
||||
|
||||
// Second pass: collect exclude_paths for directory entries
|
||||
// Second pass: collect exclusions for directory entries
|
||||
for exp_idx in 0..explicit_end {
|
||||
for glob_idx in glob_start..total {
|
||||
if glob_to_remove.contains(&glob_idx) {
|
||||
|
|
@ -811,9 +825,24 @@ fn merge_specializations(dotfiles: &mut Vec<DotfileConfig>, glob_count: usize) {
|
|||
let exp_target = dotfiles[exp_idx].target.clone();
|
||||
let glob_target = &dotfiles[glob_idx].target;
|
||||
|
||||
// Target-based exclusion: explicit target is inside glob target directory
|
||||
if exp_target.starts_with(glob_target) && exp_target != *glob_target {
|
||||
dotfiles[glob_idx].exclude_paths.push(exp_target);
|
||||
}
|
||||
|
||||
// Source-based exclusion: explicit source is inside glob source directory
|
||||
// Store path relative to the directory source so it matches after strip_prefix
|
||||
let exp_source = dotfiles[exp_idx].source.clone();
|
||||
let glob_source = dotfiles[glob_idx].source.clone();
|
||||
|
||||
if exp_source.starts_with(&glob_source)
|
||||
&& exp_source != glob_source
|
||||
&& let Ok(relative) = exp_source.strip_prefix(&glob_source)
|
||||
{
|
||||
dotfiles[glob_idx]
|
||||
.exclude_sources
|
||||
.push(relative.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -165,7 +165,9 @@ impl Deployer {
|
|||
}
|
||||
}
|
||||
|
||||
if !self.config.dry_run {
|
||||
self.state.lock().unwrap().save()?;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +222,9 @@ impl Deployer {
|
|||
}
|
||||
}
|
||||
|
||||
if !self.config.dry_run {
|
||||
self.state.lock().unwrap().save()?;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
|
|
@ -320,16 +324,40 @@ impl Deployer {
|
|||
let mut any_updated = false;
|
||||
let mut any_created = false;
|
||||
|
||||
for (src_file, tgt_file, status) in changed_files {
|
||||
// Skip files that have explicit specializations
|
||||
// Filter out excluded files before processing
|
||||
let changed_files: Vec<_> = changed_files
|
||||
.into_iter()
|
||||
.filter(|(src_file, tgt_file, _)| {
|
||||
// Skip files excluded by target path
|
||||
if dotfile
|
||||
.exclude_paths
|
||||
.iter()
|
||||
.any(|ex| tgt_file.starts_with(ex) || *ex == tgt_file)
|
||||
.any(|ex| tgt_file.starts_with(ex) || ex == tgt_file)
|
||||
{
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
// Skip files excluded by source path
|
||||
if dotfile
|
||||
.exclude_sources
|
||||
.iter()
|
||||
.any(|ex| src_file.strip_prefix(source) == Ok(ex.as_path()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
// If all files are excluded, skip creating the directory entirely
|
||||
if changed_files.is_empty() {
|
||||
return Ok(DeployedFile {
|
||||
source: source.to_path_buf(),
|
||||
target: target.to_path_buf(),
|
||||
action: DeployAction::Unchanged,
|
||||
});
|
||||
}
|
||||
|
||||
for (src_file, tgt_file, status) in changed_files {
|
||||
match status {
|
||||
SyncStatus::NotDeployed
|
||||
| SyncStatus::TargetMissing
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ pub mod package;
|
|||
pub mod state;
|
||||
|
||||
pub use config::Config;
|
||||
pub use deploy::{DeployResult, Deployer};
|
||||
pub use deploy::{DeployAction, DeployResult, Deployer};
|
||||
pub use encryption::AgeEncryption;
|
||||
pub use hooks::HookRunner;
|
||||
pub use os::OsInfo;
|
||||
|
|
|
|||
|
|
@ -145,6 +145,12 @@ pub struct Dotfile {
|
|||
pub deploy: DeployMode,
|
||||
pub link_patterns: Vec<String>,
|
||||
pub copy_patterns: Vec<String>,
|
||||
/// Span of the source expression (for error reporting).
|
||||
pub source_span: Option<Span>,
|
||||
/// Span of the target expression (for error reporting).
|
||||
pub target_span: Option<Span>,
|
||||
/// Span of the when expression (for error reporting).
|
||||
pub when_span: Option<Span>,
|
||||
}
|
||||
|
||||
/// Package installation declaration.
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ pub fn path_extension(args: &[Value]) -> Result<Value, EvalError> {
|
|||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
pub fn home() -> Result<Value, EvalError> {
|
||||
pub fn home_dir() -> Result<Value, EvalError> {
|
||||
Ok(Value::Path(dirs::home_dir().unwrap_or_default()))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ pub async fn call_builtin(
|
|||
"path_parent" => io::path_parent(args),
|
||||
"path_filename" => io::path_filename(args),
|
||||
"path_extension" => io::path_extension(args),
|
||||
"home" => io::home(),
|
||||
"home_dir" => io::home_dir(),
|
||||
"config_dir" => io::config_dir(),
|
||||
"data_dir" => io::data_dir(),
|
||||
"cache_dir" => io::cache_dir(),
|
||||
|
|
|
|||
|
|
@ -319,8 +319,10 @@ pub struct DotfileConfig {
|
|||
pub deploy: DeployMode,
|
||||
pub link_patterns: Vec<String>,
|
||||
pub copy_patterns: Vec<String>,
|
||||
/// Files to skip during directory deploy (specialized by explicit dotfile blocks).
|
||||
/// Target paths to skip during directory deploy (specialized by explicit dotfile blocks).
|
||||
pub exclude_paths: Vec<PathBuf>,
|
||||
/// Source paths to skip during directory deploy (when explicit block targets elsewhere).
|
||||
pub exclude_sources: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Evaluated package configuration.
|
||||
|
|
@ -679,6 +681,7 @@ impl Evaluator {
|
|||
link_patterns: dotfile.link_patterns.clone(),
|
||||
copy_patterns: dotfile.copy_patterns.clone(),
|
||||
exclude_paths: vec![],
|
||||
exclude_sources: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ impl MacroExpander {
|
|||
deploy: dotfile.deploy,
|
||||
link_patterns: dotfile.link_patterns.clone(),
|
||||
copy_patterns: dotfile.copy_patterns.clone(),
|
||||
source_span: dotfile.source_span.clone(),
|
||||
target_span: dotfile.target_span.clone(),
|
||||
when_span: dotfile.when_span.clone(),
|
||||
}),
|
||||
|
||||
Statement::Package(pkg) => Statement::Package(Box::new(Package {
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ impl Parser {
|
|||
fn dotfile_parser() -> impl chumsky::Parser<Token, Dotfile, Error = Simple<Token>> {
|
||||
let field = Self::field_name_parser()
|
||||
.then_ignore(just(Token::Eq))
|
||||
.then(Self::expr_parser());
|
||||
.then(Self::expr_parser().map_with_span(|expr, span| (expr, span)));
|
||||
|
||||
just(Token::Dotfile)
|
||||
.ignore_then(just(Token::Colon))
|
||||
|
|
@ -249,12 +249,24 @@ impl Parser {
|
|||
deploy: DeployMode::default(),
|
||||
link_patterns: Vec::new(),
|
||||
copy_patterns: Vec::new(),
|
||||
source_span: None,
|
||||
target_span: None,
|
||||
when_span: None,
|
||||
};
|
||||
for (name, value) in fields {
|
||||
for (name, (value, span)) in fields {
|
||||
match name.as_str() {
|
||||
"source" => dotfile.source = value,
|
||||
"target" => dotfile.target = value,
|
||||
"when" => dotfile.when = Some(value),
|
||||
"source" => {
|
||||
dotfile.source = value;
|
||||
dotfile.source_span = Some(span);
|
||||
}
|
||||
"target" => {
|
||||
dotfile.target = value;
|
||||
dotfile.target_span = Some(span);
|
||||
}
|
||||
"when" => {
|
||||
dotfile.when = Some(value);
|
||||
dotfile.when_span = Some(span);
|
||||
}
|
||||
"template" => {
|
||||
if let Expr::Literal(Literal::Bool(b)) = value {
|
||||
dotfile.template = Some(b);
|
||||
|
|
|
|||
|
|
@ -297,6 +297,7 @@ mod tests {
|
|||
link_patterns: Vec::new(),
|
||||
copy_patterns: Vec::new(),
|
||||
exclude_paths: Vec::new(),
|
||||
exclude_sources: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -285,7 +285,8 @@ impl TypeChecker {
|
|||
}
|
||||
|
||||
Statement::Dotfile(dotfile) => {
|
||||
let source_ty = self.infer_expr(&dotfile.source, &stmt.span);
|
||||
let source_span = dotfile.source_span.as_ref().unwrap_or(&stmt.span);
|
||||
let source_ty = self.infer_expr(&dotfile.source, source_span);
|
||||
// dotfile: source accepts path, str (pattern with wildcards), or list
|
||||
if !matches!(
|
||||
source_ty,
|
||||
|
|
@ -294,24 +295,26 @@ impl TypeChecker {
|
|||
self.errors.push(TypeError::TypeMismatch {
|
||||
expected: "path, str, or [path]".to_string(),
|
||||
got: source_ty.display(),
|
||||
span: stmt.span.clone(),
|
||||
span: source_span.clone(),
|
||||
});
|
||||
}
|
||||
let target_ty = self.infer_expr(&dotfile.target, &stmt.span);
|
||||
let target_span = dotfile.target_span.as_ref().unwrap_or(&stmt.span);
|
||||
let target_ty = self.infer_expr(&dotfile.target, target_span);
|
||||
if matches!(target_ty, Type::List(_)) {
|
||||
self.errors.push(TypeError::TypeMismatch {
|
||||
expected: "path".to_string(),
|
||||
got: target_ty.display(),
|
||||
span: stmt.span.clone(),
|
||||
span: target_span.clone(),
|
||||
});
|
||||
}
|
||||
if let Some(ref when) = dotfile.when {
|
||||
let when_ty = self.infer_expr(when, &stmt.span);
|
||||
let when_span = dotfile.when_span.as_ref().unwrap_or(&stmt.span);
|
||||
let when_ty = self.infer_expr(when, when_span);
|
||||
if !when_ty.is_compatible(&Type::Bool) {
|
||||
self.errors.push(TypeError::TypeMismatch {
|
||||
expected: "bool".to_string(),
|
||||
got: when_ty.display(),
|
||||
span: stmt.span.clone(),
|
||||
span: when_span.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -739,7 +742,7 @@ impl TypeChecker {
|
|||
"read_file" | "read_file_lines" => Type::Str,
|
||||
"file_exists" | "dir_exists" | "is_symlink" => Type::Bool,
|
||||
"list_dir" | "walk_dir" => Type::List(Box::new(Type::Path)),
|
||||
"home" | "config_dir" | "data_dir" | "cache_dir" | "temp_dir" | "temp_file" => {
|
||||
"home_dir" | "config_dir" | "data_dir" | "cache_dir" | "temp_dir" | "temp_file" => {
|
||||
Type::Path
|
||||
}
|
||||
"path_join" | "path_parent" | "path_filename" | "path_extension" | "read_link" => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue