use serde_json::{Map, Value};
use crate::analysis::findings::Finding;
use crate::languages::runner::ToolOutputError;
use crate::languages::spec::{OutputFormat, ToolSpec};
mod cargo;
mod credo;
mod json;
mod lines;
mod phpcs;
mod position;
mod rubocop;
mod sarif;
mod shellcheck;
mod sqlfluff;
pub fn parse_output(
spec: &ToolSpec,
output: &str,
root_name: &str,
) -> Result<Vec<Finding>, ToolOutputError> {
match spec.output_format {
OutputFormat::Lines => Ok(lines::parse_lines(spec, output)),
OutputFormat::Json => json::parse_json(spec, output, root_name),
OutputFormat::Position => Ok(position::parse_positions(spec, output)),
OutputFormat::Tsc => Ok(position::parse_tsc(output)),
OutputFormat::Cargo => cargo::parse_cargo(spec, output),
OutputFormat::Sarif => sarif::parse_sarif(spec, output),
OutputFormat::Ktlint => json::parse_ktlint(spec, output, root_name),
OutputFormat::Shellcheck => shellcheck::parse_shellcheck(spec, output, root_name),
OutputFormat::Rubocop => rubocop::parse_rubocop(spec, output, root_name),
OutputFormat::Phpcs => phpcs::parse_phpcs(spec, output),
OutputFormat::Credo => credo::parse_credo(spec, output, root_name),
OutputFormat::Sqlfluff => sqlfluff::parse_sqlfluff(spec, output, root_name),
OutputFormat::Msbuild => Ok(position::parse_msbuild(output)),
}
}
pub(super) fn json_payload(
spec: &ToolSpec,
output: &str,
) -> Result<Option<Value>, ToolOutputError> {
let trimmed = output.trim();
if trimmed.is_empty() {
return Ok(None);
}
serde_json::from_str(trimmed)
.map(Some)
.map_err(|err| ToolOutputError(format!("{} produced unparseable JSON: {err}", spec.name)))
}
pub(super) fn expect_array<'a>(
spec: &ToolSpec,
payload: &'a Value,
) -> Result<&'a Vec<Value>, ToolOutputError> {
payload.as_array().ok_or_else(|| {
ToolOutputError(format!(
"{}: expected a JSON array, got {}",
spec.name,
json_kind_name(payload)
))
})
}
pub(super) fn expect_keyed_array<'a>(
spec: &ToolSpec,
payload: &'a Value,
key: &str,
) -> Result<&'a Vec<Value>, ToolOutputError> {
match payload.get(key) {
Some(value) => value.as_array().ok_or_else(|| {
ToolOutputError(format!(
"{}: expected {key} to be an array, got {}",
spec.name,
json_kind_name(value)
))
}),
None => Err(ToolOutputError(format!(
"{}: expected an {key} array, got {}",
spec.name,
json_kind_name(payload)
))),
}
}
pub(super) fn expect_keyed_object<'a>(
spec: &ToolSpec,
payload: &'a Value,
key: &str,
) -> Result<&'a Map<String, Value>, ToolOutputError> {
match payload.get(key) {
Some(value) => value.as_object().ok_or_else(|| {
ToolOutputError(format!(
"{}: expected {key} to be an object, got {}",
spec.name,
json_kind_name(value)
))
}),
None => Err(ToolOutputError(format!(
"{}: expected a {key} object, got {}",
spec.name,
json_kind_name(payload)
))),
}
}
pub(super) fn json_kind_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
pub(super) fn path_or_root(field: Option<&Value>, root_name: &str) -> String {
field
.and_then(Value::as_str)
.map_or_else(|| root_name.to_owned(), str::to_owned)
}