use std::path::Path;
use laterite_ags4_validator::{CheckOptions, fix_document_selective, fixes::FixRisk};
use super::{Finding, Source, bad_encoding, convert, resolve_edition, validator_kind};
use crate::{Error, ErrorKind};
#[must_use]
pub fn fixable_rules() -> Vec<&'static str> {
laterite_ags4_validator::fixes::FIXABLE_RULE_LABELS.to_vec()
}
pub struct Fix {
source: Source,
edition: Option<String>,
encoding: Option<String>,
risky: bool,
only: Option<Vec<String>>,
exclude: Vec<String>,
}
fn pending(source: Source) -> Fix {
Fix {
source,
edition: None,
encoding: None,
risky: false,
only: None,
exclude: Vec::new(),
}
}
#[must_use]
pub fn fix(path: impl AsRef<Path>) -> Fix {
pending(Source::Path(path.as_ref().to_path_buf()))
}
#[must_use]
pub fn fix_bytes(bytes: impl Into<Vec<u8>>) -> Fix {
pending(Source::Bytes(bytes.into()))
}
#[must_use]
pub fn fix_str(text: impl Into<String>) -> Fix {
pending(Source::Text(text.into()))
}
impl Fix {
#[must_use]
pub fn edition(mut self, edition: impl Into<String>) -> Fix {
self.edition = Some(edition.into());
self
}
#[must_use]
pub fn encoding(mut self, label: impl Into<String>) -> Fix {
self.encoding = Some(label.into());
self
}
#[must_use]
pub fn risky(mut self, yes: bool) -> Fix {
self.risky = yes;
self
}
#[must_use]
pub fn only<I, S>(mut self, rules: I) -> Fix
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.only = Some(rules.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn exclude<I, S>(mut self, rules: I) -> Fix
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.exclude = rules.into_iter().map(Into::into).collect();
self
}
pub fn run(self) -> Result<Fixed, Error> {
let raw = match &self.source {
Source::Path(p) => std::fs::read(p).map_err(|e| {
Error::with_source(ErrorKind::Io, format!("cannot read {}", p.display()), e)
})?,
Source::Bytes(b) => b.clone(),
Source::Text(s) => s.clone().into_bytes(),
};
let label = match &self.source {
Source::Text(_) => None,
_ => self.encoding.as_deref(),
};
let opts = CheckOptions {
dict_version: self.edition.as_deref().map(resolve_edition).transpose()?,
custom_dict: None,
include_warnings: true,
include_fyi: false,
check_files: false,
encoding: laterite_ags4_parse::resolve_encoding(label)
.ok_or_else(|| bad_encoding(label.unwrap_or_default()))?,
};
let outcome =
fix_document_selective(&raw, &opts, self.risky, self.only.as_deref(), &self.exclude)
.map_err(|e| {
let subject = match &self.source {
Source::Path(p) => format!("cannot fix {}", p.display()),
Source::Bytes(b) => format!("cannot fix {} bytes", b.len()),
Source::Text(s) => format!("cannot fix {} characters", s.chars().count()),
};
Error::with_source(validator_kind(e.kind()), subject, e)
})?;
Ok(Fixed {
text: String::from_utf8(outcome.fixed).map_err(|e| {
Error::with_source(
ErrorKind::Other,
"the fixer produced bytes that are not UTF-8",
e,
)
})?,
findings: convert(outcome.residual),
applied: outcome.applied.iter().map(Repair::from_engine).collect(),
edition: outcome.dict_version.as_str().to_string(),
risky_available: outcome.risky_available,
})
}
pub fn to_path(self, path: impl AsRef<Path>) -> Result<Fixed, Error> {
let fixed = self.run()?;
fixed.save(path)?;
Ok(fixed)
}
}
pub struct Fixed {
text: String,
findings: Vec<Finding>,
applied: Vec<Repair>,
edition: String,
risky_available: usize,
}
impl Fixed {
#[must_use]
pub fn bytes(&self) -> &[u8] {
self.text.as_bytes()
}
#[must_use]
pub fn into_bytes(self) -> Vec<u8> {
self.text.into_bytes()
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub fn into_text(self) -> String {
self.text
}
#[must_use]
pub fn findings(&self) -> &[Finding] {
&self.findings
}
#[must_use]
pub fn applied(&self) -> &[Repair] {
&self.applied
}
#[must_use]
pub fn fixes_applied(&self) -> usize {
self.applied.len()
}
#[must_use]
pub fn risky_available(&self) -> usize {
self.risky_available
}
#[must_use]
pub fn edition(&self) -> &str {
&self.edition
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<(), Error> {
let path = path.as_ref();
std::fs::write(path, self.bytes()).map_err(|e| {
Error::with_source(ErrorKind::Io, format!("cannot write {}", path.display()), e)
})
}
}
pub struct Repair {
kind: String,
label: String,
rule: String,
line: Option<u32>,
risky: bool,
}
impl Repair {
fn from_engine(fix: &laterite_ags4_validator::Fix) -> Repair {
Repair {
kind: serde_json::to_value(fix.kind)
.ok()
.and_then(|v| v.as_str().map(str::to_string))
.unwrap_or_default(),
label: fix.label.clone(),
rule: fix.rule.clone(),
line: fix.line,
risky: fix.risk == FixRisk::Risky,
}
}
#[must_use]
pub fn kind(&self) -> &str {
&self.kind
}
#[must_use]
pub fn label(&self) -> &str {
&self.label
}
#[must_use]
pub fn rule(&self) -> &str {
&self.rule
}
#[must_use]
pub fn line(&self) -> Option<u32> {
self.line
}
#[must_use]
pub fn is_risky(&self) -> bool {
self.risky
}
}
impl std::fmt::Debug for Fix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Fix")
.field("source", &self.source.describe())
.field("edition", &self.edition)
.field("encoding", &self.encoding)
.field("risky", &self.risky)
.field("only", &self.only)
.field("exclude", &self.exclude)
.finish()
}
}
impl std::fmt::Debug for Fixed {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Fixed")
.field("bytes", &self.text.len())
.field("findings", &self.findings.len())
.field("applied", &self.applied.len())
.field("edition", &self.edition)
.field("risky_available", &self.risky_available)
.finish()
}
}
impl std::fmt::Debug for Repair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Repair")
.field("kind", &self.kind)
.field("label", &self.label)
.field("rule", &self.rule)
.field("line", &self.line)
.field("risky", &self.risky)
.finish()
}
}