use std::{
io::{BufRead, BufReader, Read, Write},
os::unix::process::CommandExt as _,
};
use nix::unistd::Pid;
use regex::Captures;
use crate::prelude::*;
use std::process::Child;
fn get_root_dir(filename: &str, workspace: &Workspace, root_markers: &[String]) -> Result<String> {
let starting_path = std::path::PathBuf::from(filename);
if !root_markers.is_empty() {
let mut path = starting_path.as_path();
log::info!("path = {path:?}");
while path.parent().is_some() {
path = path.parent().unwrap();
let path_buf = path.to_path_buf();
log::trace!("path_buf = {path_buf:?}");
if workspace.folders().any(|folder| folder == &path_buf)
|| root_markers
.iter()
.any(|marker| path_buf.join(marker).exists())
{
return Ok(path_buf.to_str().ok_or("invalid root dir")?.to_string());
}
}
}
let basedir = starting_path
.parent()
.ok_or("path has no basedir")?
.to_str()
.ok_or("invalid basedir path")?
.to_string();
log::info!("no root directory found for file {filename}, using {basedir}");
Ok(basedir)
}
pub fn run_linter(
diagnostics_manager: &mut DiagnosticsManager,
linter_config: PicklsLinterConfig,
workspace: &Workspace,
root_markers: &[String],
max_linter_count: usize,
file_content: Option<String>,
uri: Uri,
version: DocumentVersion,
) -> Result<Pid> {
let (mut cmd, root_dir) = {
let filename = uri.path().as_str();
let mut cmd = Command::new(&linter_config.program);
let mut args = linter_config.args.clone();
for arg in args.iter_mut() {
*arg = arg.replace("$filename", filename);
}
let root_dir: String = get_root_dir(filename, workspace, root_markers)?;
log::info!(
"running linter {program} with root_dir={root_dir}",
program = linter_config.program
);
cmd.process_group(0)
.args(args)
.current_dir(root_dir.clone())
.stdin(std::process::Stdio::piped());
if linter_config.use_stderr {
cmd.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped());
} else {
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null());
}
(cmd, root_dir)
};
log::info!("spawning {cmd:?}...");
let mut child: Child = cmd.spawn()?;
let child_pid = run_linter_core(
diagnostics_manager,
linter_config,
max_linter_count,
file_content,
uri,
version,
root_dir,
&mut child,
);
let _ = child
.wait()
.inspect_err(|e| panic!("Failed to wait for child process: {child:?}: {e}"));
child_pid
}
fn run_linter_core(
diagnostics_manager: &mut DiagnosticsManager,
linter_config: PicklsLinterConfig,
max_linter_count: usize,
file_content: Option<String>,
uri: Uri,
version: DocumentVersion,
root_dir: String,
child: &mut Child,
) -> Result<Pid> {
log::info!(
"{program} PID={pid}",
program = linter_config.program,
pid = child.id()
);
if linter_config.use_stdin {
let mut stdin: std::process::ChildStdin = child.stdin.take().expect("Failed to open stdin");
if let Some(file_content) = file_content {
log::info!(
"writing to `{program}`'s stdin: '{preamble}...'",
program = linter_config.program,
preamble = String::from(&file_content[..std::cmp::min(file_content.len(), 20)])
.replace("\n", "\\n")
);
stdin.write_all(file_content.as_bytes())?;
}
drop(stdin);
}
let child_pid = Pid::from_raw(child.id() as i32);
if let Err(error) = if linter_config.use_stderr {
ingest_linter_errors(
uri,
version,
diagnostics_manager,
&root_dir,
max_linter_count,
&linter_config,
BufReader::new(child.stderr.take().expect("Failed to take stderr")),
)
} else {
ingest_linter_errors(
uri,
version,
diagnostics_manager,
&root_dir,
max_linter_count,
&linter_config,
BufReader::new(child.stdout.take().expect("Failed to take stdout")),
)
} {
log::error!("[run_linter/spawn-ingest] error: {error:?}");
}
child
.wait()
.inspect(|status| {
log::info!(
"linter program '{program}' exited with status: {status:?} [pid={pid}]",
program = linter_config.program,
pid = child_pid,
);
})
.inspect_err(|err| {
log::warn!(
"linter program '{program}' error: {err}",
program = linter_config.program,
)
})?;
Ok(child_pid)
}
fn convert_capture_to_diagnostic(
absolute_filename: &str,
linter_config: &PicklsLinterConfig,
caps: Captures,
prior_line: &Option<String>,
) -> Option<PicklsDiagnostic> {
let caps_len = caps.len();
let description: Option<String> = match linter_config.description_match {
None => None,
Some(-1) => prior_line.as_ref().map(|s| s.trim().to_string()),
Some(i) if i > 0 => {
if linter_config.line_match >= caps_len {
log::error!(
"invalid description_match in linter configuration of `{program}`: pattern only captures {caps_len} groups but description_match = {i}.",
program = linter_config.program
);
return None;
}
caps.get(i as usize).map(|x| x.as_str().to_string())
}
Some(value) => {
log::error!(
"invalid description_match in linter configuration of `{program}`: description_match={value}",
program = linter_config.program
);
None
}
};
let filename = linter_config
.filename_match
.and_then(|i| caps.get(i).map(|x| x.as_str().to_string()))
.unwrap_or_else(|| absolute_filename.to_string());
if linter_config.line_match >= caps_len {
log::error!(
"invalid line_match in linter configuration of `{program}`: pattern only captures {caps_len} groups but line_match = {line_match}.",
line_match = linter_config.line_match,
program = linter_config.program
);
return None;
}
let line: u32 = caps
.get(linter_config.line_match)
.unwrap()
.as_str()
.parse()
.ok()?;
let start_column = linter_config
.start_col_match
.and_then(|i| caps.get(i)?.as_str().parse().ok());
let end_column = linter_config
.end_col_match
.and_then(|i| caps.get(i)?.as_str().parse().ok());
let severity: Option<PicklsDiagnosticSeverity> = linter_config.severity_match.and_then(|i| {
Some(PicklsDiagnosticSeverity {
severity: caps.get(i)?.as_str().to_string(),
})
});
Some(PicklsDiagnostic {
linter: linter_config.program.clone(),
filename,
line,
start_column,
end_column,
severity,
description,
})
}
fn ingest_linter_errors(
uri: Uri,
version: DocumentVersion,
diagnostics_manager: &mut DiagnosticsManager,
root_dir: &str,
max_linter_count: usize,
linter_config: &PicklsLinterConfig,
child_stdout: BufReader<impl Read>,
) -> Result<()> {
let re = Regex::new(&linter_config.pattern).map_err(|e| {
format!(
"Invalid regex [pattern={pattern}, error={e}]",
pattern = linter_config.pattern
)
})?;
let mut lsp_diagnostics: Vec<Diagnostic> = Default::default();
let mut prior_line: Option<String> = None;
let root_dir = std::path::PathBuf::from(root_dir);
let realpath_for_uri = match std::fs::canonicalize(uri.path().as_str()) {
Ok(path) => path,
Err(_) => {
std::path::absolute(uri.path().as_str())?
}
};
for line in child_stdout.lines() {
let line = line?;
if let Some(caps) = re.captures(&line) {
log::trace!("caps: {caps:?}");
if let Some(lsp_diagnostic) =
convert_capture_to_diagnostic(uri.path().as_str(), linter_config, caps, &prior_line)
{
let mut path = std::path::PathBuf::from(lsp_diagnostic.filename.clone());
if path.is_relative() {
path = root_dir.join(path);
}
let realpath_for_diagnostic = path.canonicalize()?;
if realpath_for_uri == realpath_for_diagnostic {
lsp_diagnostics.push(lsp_diagnostic.into());
} else {
log::warn!(
"ignoring diagnostic for {uri} because it is not in the current document [filename={realpath_for_diagnostic:?}]",
uri = uri.as_str(),
);
}
}
}
prior_line = Some(line);
}
log::info!(
"publishing diagnostics [linter={linter_name}, count={count}]",
linter_name = linter_config.program,
count = lsp_diagnostics.len()
);
diagnostics_manager.update_diagnostics(
uri.clone(),
linter_config.program.clone(),
max_linter_count,
version,
lsp_diagnostics,
)
}
pub fn run_formatter(
formatter_config: &PicklsFormatterConfig,
workspace: &Workspace,
root_markers: &[String],
file_content: String,
uri: Uri,
) -> Result<String> {
let mut cmd = {
let filename = uri.path().as_str();
let mut cmd = Command::new(&formatter_config.program);
let mut args = formatter_config.args.clone();
for arg in args.iter_mut() {
*arg = arg.replace("$filename", filename);
}
let root_dir: String = get_root_dir(filename, workspace, root_markers)?;
log::info!(
"running formatter {program} with root_dir={root_dir}",
program = formatter_config.program
);
cmd.process_group(0).args(args).current_dir(root_dir);
if formatter_config.use_stdin {
cmd.stdin(std::process::Stdio::piped());
}
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
cmd
};
log::info!("spawning {cmd:?} [stdin={}]", formatter_config.use_stdin);
let mut child = cmd.spawn()?;
let mut stdout = child.stdout.take().expect("Failed to open stdout");
let mut stderr = child.stderr.take().expect("Failed to open stderr");
if formatter_config.use_stdin {
let mut stdin = child.stdin.take().expect("Failed to open stdin");
stdin.write_all(file_content.as_bytes())?;
}
let mut formatted_content = String::new();
let mut error_text = String::new();
match (
stdout.read_to_string(&mut formatted_content),
stderr.read_to_string(&mut error_text),
) {
(Ok(stdout_len), Ok(stderr_len)) => {
log::info!("stdout_len = {stdout_len}, stderr_len = {stderr_len}");
if formatter_config.stderr_indicates_error && stderr_len != 0 {
log::error!(
"Failed to format file {uri}: {error_text}",
uri = uri.as_str()
);
return Err(Error::new("Failed to format file"));
}
}
(Err(err), Err(err2)) => {
log::error!(
"Failed to format file {uri}: {err} & {err2}",
uri = uri.as_str()
);
return Err(Error::new("Failed to format file"));
}
(Err(err), _) | (_, Err(err)) => {
log::error!("Failed to format file {uri}: {err}", uri = uri.as_str());
return Err(Error::new("Failed to format file"));
}
};
let exit_status = child.wait()?;
if exit_status.success() {
Ok(formatted_content)
} else {
log::error!("Failed to format file {uri}", uri = uri.as_str());
Err(Error::new("Failed to format file"))
}
}