use std::{
path::{Path, PathBuf},
process::Command,
};
use clingwrap::runner::{CommandError, CommandRunner};
use crate::{
action::{PostPlanAction, PrePlanAction, UnsafeAction},
action_impl::bash_snippet,
config::Config,
project::{Project, Projects},
};
#[derive(Debug)]
pub struct Linter<'a> {
config: &'a Config,
projects: &'a Projects,
lints: Vec<Box<dyn Lint>>,
}
impl<'a> Linter<'a> {
pub fn new(config: &'a Config, projects: &'a Projects) -> Self {
Self {
config,
projects,
lints: vec![
Box::new(RsyncTarget),
Box::new(HttpGet),
Box::new(ShellCheck),
Box::new(DebDputVersions),
Box::new(OldWorkspacePath),
],
}
}
pub fn lint(&self) -> Result<(), LinterError> {
for (name, project) in self.projects.iter() {
for lint in self.lints.iter() {
lint.check(self.config, name, project)?;
}
}
Ok(())
}
}
trait Lint: std::fmt::Debug {
#[allow(dead_code)]
fn name(&self) -> &'static str;
fn check(
&self,
config: &Config,
project_name: &str,
project: &Project,
) -> Result<(), LinterError>;
}
#[derive(Debug)]
struct RsyncTarget;
impl Lint for RsyncTarget {
fn name(&self) -> &'static str {
"rsync_target"
}
fn check(
&self,
config: &Config,
project_name: &str,
project: &Project,
) -> Result<(), LinterError> {
let uses_rsync = project
.post_plan()
.iter()
.any(|a| matches!(a, PostPlanAction::Rsync | PostPlanAction::Rsync2));
let configs_rsync = config.rsync_target_for_project(project_name).is_some();
if uses_rsync && !configs_rsync {
return Err(LinterError::rsync_target_missing(project_name));
}
Ok(())
}
}
#[derive(Debug)]
struct HttpGet;
impl Lint for HttpGet {
fn name(&self) -> &'static str {
"http_get"
}
fn check(
&self,
_config: &Config,
project_name: &str,
project: &Project,
) -> Result<(), LinterError> {
for action in project.pre_plan() {
let mut filenames = vec![];
if let PrePlanAction::HttpGet { items } = action {
for item in items {
if filenames.contains(&item.filename) {
return Err(LinterError::http_get_duplicate_filename(
project_name,
&item.filename,
));
}
filenames.push(item.filename.clone());
}
}
}
Ok(())
}
}
#[derive(Debug)]
struct ShellCheck;
impl Lint for ShellCheck {
fn name(&self) -> &'static str {
"shellcheck"
}
fn check(
&self,
_config: &Config,
_project_name: &str,
project: &Project,
) -> Result<(), LinterError> {
for action in project.plan() {
if let UnsafeAction::Shell { shell } = action {
let mut cmd = Command::new("shellcheck");
cmd.args(["-s", "bash", "-"]);
let mut runner = CommandRunner::new(cmd);
runner.feed_stdin(bash_snippet(shell).as_bytes());
runner.capture_stdout();
runner.capture_stderr();
match runner.execute() {
Ok(_) => (),
Err(CommandError::CommandFailed { output, .. }) => {
return Err(LinterError::shellcheck(output.stdout))
}
Err(err) => return Err(LinterError::ShellcheckError(err)),
}
}
}
Ok(())
}
}
#[derive(Debug)]
struct DebDputVersions;
impl Lint for DebDputVersions {
fn name(&self) -> &'static str {
"deb_dput_versions"
}
fn check(
&self,
_config: &Config,
_project_name: &str,
project: &Project,
) -> Result<(), LinterError> {
let has_deb = project
.plan()
.iter()
.any(|action| matches!(action, UnsafeAction::Deb));
let has_deb2 = project
.plan()
.iter()
.any(|action| matches!(action, UnsafeAction::Deb2));
let has_dput = project
.post_plan()
.iter()
.any(|action| matches!(action, PostPlanAction::Dput));
let has_dput2 = project
.post_plan()
.iter()
.any(|action| matches!(action, PostPlanAction::Dput2));
if (has_deb && has_dput2) || (has_deb2 && has_dput) {
Err(LinterError::IncompatibleDebianActions)
} else {
Ok(())
}
}
}
#[derive(Debug)]
struct OldWorkspacePath;
impl Lint for OldWorkspacePath {
fn name(&self) -> &'static str {
"/workspace_used"
}
fn check(
&self,
_config: &Config,
_project_name: &str,
project: &Project,
) -> Result<(), LinterError> {
let old_path_used = project.plan().iter().any(|action| {
if let UnsafeAction::Shell { shell } = action {
shell.contains("/workspace")
} else {
false
}
});
if old_path_used {
Err(LinterError::OldWorkspacePath)
} else {
Ok(())
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum LinterError {
#[error(
"rsync or rsync2 action used in project {project}, but no `rsync` target in configuration"
)]
RsyncTargetMissing {
project: String,
},
#[error("in project {project_name} http_get pre-plan action uses the same filename for more than one item: {filename}")]
HttpGetDuplicateFilename {
project_name: String,
filename: PathBuf,
},
#[error("shellcheck found problems: {0}")]
Shellcheck(String),
#[error("failed to run shellcheck")]
ShellcheckError(#[source] CommandError),
#[error("has incompible deb/deb2 and dput/dput2 pairs, use only one version")]
IncompatibleDebianActions,
#[error("old path /workspace used, use /ci instead")]
OldWorkspacePath,
}
impl LinterError {
fn rsync_target_missing(project_name: &str) -> Self {
Self::RsyncTargetMissing {
project: project_name.into(),
}
}
fn http_get_duplicate_filename(project_name: &str, filename: &Path) -> Self {
Self::HttpGetDuplicateFilename {
project_name: project_name.into(),
filename: filename.into(),
}
}
fn shellcheck(output: Vec<u8>) -> Self {
let output = String::from_utf8_lossy(&output).to_string();
Self::Shellcheck(output)
}
}