shuck-linter 0.0.41

Lint rule engine and checker for shell scripts
Documentation
use crate::{Checker, Rule, Violation};

pub struct RmRootishTarget;

impl Violation for RmRootishTarget {
    fn rule() -> Rule {
        Rule::RmRootishTarget
    }

    fn message(&self) -> String {
        "recursive `rm` targets a root-like directory".to_owned()
    }
}

pub fn rm_rootish_target(checker: &mut Checker) {
    let spans = checker
        .facts()
        .command_facts()
        .structural_commands()
        .filter(|fact| fact.effective_name_is("rm"))
        .filter_map(|fact| fact.options().rm())
        .flat_map(|rm| rm.rootish_path_spans(checker.source()).iter().copied())
        .collect::<Vec<_>>();

    checker.report_all(spans, || RmRootishTarget);
}

#[cfg(test)]
mod tests {
    use crate::test::test_snippet;
    use crate::{LinterSettings, Rule};

    #[test]
    fn reports_recursive_rm_on_root_or_home_roots() {
        let source = "#!/bin/sh\nrm -rf /\nrm -rf /*\nrm -rf /**\nrm -rf /./\nrm -rf /./*\nrm -rf /..\nrm -rf /../*\nrm -rf \"$HOME\"\nrm -rf \"${HOME}\"/*\nrm -rf \"${HOME}\"/**\nrm -rf \"$HOME\"/.\nrm -rf \"$HOME\"/./*\nrm -rf \"${HOME:?}\"\nrm -rf \"${HOME:?}\"/*\nrm -rf \"${HOME:?}\"/.\nrm -rf ~\nrm -rf ~/*\nrm -rf ~/**\nrm -rf ~/.\nrm -rf ~/./*\nrm -rf ~root/*\n";
        let diagnostics = test_snippet(source, &LinterSettings::for_rule(Rule::RmRootishTarget));

        assert_eq!(
            diagnostics
                .iter()
                .map(|diagnostic| diagnostic.span.slice(source))
                .collect::<Vec<_>>(),
            vec![
                "/",
                "/*",
                "/**",
                "/./",
                "/./*",
                "/..",
                "/../*",
                "\"$HOME\"",
                "\"${HOME}\"/*",
                "\"${HOME}\"/**",
                "\"$HOME\"/.",
                "\"$HOME\"/./*",
                "\"${HOME:?}\"",
                "\"${HOME:?}\"/*",
                "\"${HOME:?}\"/.",
                "~",
                "~/*",
                "~/**",
                "~/.",
                "~/./*",
                "~root/*"
            ]
        );
    }

    #[test]
    fn ignores_non_recursive_and_bounded_rm_targets() {
        let source = "#!/bin/sh\ndir=/tmp\nrm -f \"$HOME\"/*\nrm -rf \"$HOME\"/.cache\nrm -rf \"${HOME%/*}\"\nrm -rf \"${HOME%%/*}\"\nrm -rf \"$HOME\"/..\nrm -rf \"$HOME\"/../*\nrm -rf ~/..\nrm -rf ~/../*\nrm -rf ~0\nrm -rf ~1/*\nrm -rf ~/Downloads/*\nrm -rf /tmp/*\nrm -rf /tmp/../*\nrm -rf ~/tmp/../*\nrm -rf ~root/tmp/../*\nrm -rf \"$dir\"/*\nrm -rf \"~\"/*\nrm -rf \"$HOME\"/\"*\"\n";
        let diagnostics = test_snippet(source, &LinterSettings::for_rule(Rule::RmRootishTarget));

        assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:?}");
    }
}