use super::support::{TextBudget, invalid, too_large};
use crate::{
NotModified, PlanError, PlanEvidence, RefactorPlanLimits, TypedSubject, UncertainReference,
};
use std::collections::BTreeSet;
pub(super) fn validate_entries(
evidence: &PlanEvidence,
operation_count: usize,
limits: RefactorPlanLimits,
text: &mut TextBudget,
) -> Result<(), PlanError> {
let references = evidence.uncertain_references.as_deref().unwrap_or_default();
let omitted = evidence.not_modified.as_deref().unwrap_or_default();
let warnings = evidence.warnings.as_deref().unwrap_or_default();
let scope_entries = evidence.completeness_proof.as_ref().map_or(0, |proof| {
proof
.scope
.roots
.len()
.saturating_add(proof.scope.languages.len())
});
let total = references
.len()
.checked_add(omitted.len())
.and_then(|value| value.checked_add(warnings.len()))
.and_then(|value| value.checked_add(scope_entries))
.ok_or_else(|| too_large("evidence entry count overflow"))?;
if total > limits.max_evidence_entries {
return Err(too_large("plan contains too many evidence entries"));
}
validate_uncertainties(references, limits, text)?;
validate_omissions(omitted, operation_count, limits, text)?;
validate_warnings(warnings, text)
}
fn validate_uncertainties(
references: &[UncertainReference],
limits: RefactorPlanLimits,
text: &mut TextBudget,
) -> Result<(), PlanError> {
let mut seen = BTreeSet::new();
for (index, reference) in references.iter().enumerate() {
let field = format!("uncertainReferences[{index}]");
let location = validate_locations(
reference.path.as_deref(),
reference.file.as_deref(),
&field,
limits,
text,
)?;
let has_subject = reference
.subject
.as_ref()
.map(|subject| validate_subject(subject, &field, text))
.transpose()?
.is_some();
if location.is_none() && !has_subject {
return Err(invalid(
"uncertainty requires a path/file or typed subject",
&field,
));
}
if reference.line == Some(0) {
return Err(invalid("uncertainty line must be 1-based", &field));
}
if let Some(excerpt) = &reference.excerpt {
text.require(excerpt, &format!("{field}.excerpt"))?;
}
if !validate_uncertainty_codes(reference, &field, text)? {
return Err(invalid("uncertainty requires kind or reason", &field));
}
reject_duplicate(
&mut seen,
UncertaintyIdentity::new(reference, location),
&field,
)?;
}
Ok(())
}
fn validate_uncertainty_codes(
reference: &UncertainReference,
field: &str,
text: &mut TextBudget,
) -> Result<bool, PlanError> {
if let Some(code) = &reference.kind {
text.code(code.as_str(), &format!("{field}.kind"))?;
}
if let Some(code) = &reference.reason {
text.code(code.as_str(), &format!("{field}.reason"))?;
}
Ok(reference.kind.is_some() || reference.reason.is_some())
}
fn validate_omissions(
entries: &[NotModified],
operation_count: usize,
limits: RefactorPlanLimits,
text: &mut TextBudget,
) -> Result<(), PlanError> {
let mut seen = BTreeSet::new();
for (index, entry) in entries.iter().enumerate() {
let field = format!("notModified[{index}]");
let location = validate_locations(
entry.path.as_deref(),
entry.file.as_deref(),
&field,
limits,
text,
)?;
let has_subject = entry
.subject
.as_ref()
.map(|subject| validate_subject(subject, &field, text))
.transpose()?
.is_some();
let has_operation = entry.operation_index.is_some();
if entry
.operation_index
.is_some_and(|index| index as usize >= operation_count)
{
return Err(invalid("operationIndex is outside plan operations", &field));
}
if location.is_none() && !has_subject && !has_operation {
return Err(invalid(
"notModified requires path/file, subject, or operation",
&field,
));
}
text.require(&entry.reason, &format!("{field}.reason"))?;
reject_duplicate(&mut seen, OmissionIdentity::new(entry, location), &field)?;
}
Ok(())
}
fn validate_warnings(
warnings: &[crate::WarningCode],
text: &mut TextBudget,
) -> Result<(), PlanError> {
let mut seen = BTreeSet::new();
for (index, warning) in warnings.iter().enumerate() {
text.code(warning.as_str(), &format!("warnings[{index}]"))?;
if !seen.insert(warning.as_str()) {
return Err(invalid("duplicate warning", &format!("warnings[{index}]")));
}
}
Ok(())
}
fn validate_locations(
path: Option<&str>,
file: Option<&str>,
parent: &str,
limits: RefactorPlanLimits,
text: &mut TextBudget,
) -> Result<Option<String>, PlanError> {
for (name, value) in [("path", path), ("file", file)] {
if let Some(value) = value {
text.require(value, &format!("{parent}.{name}"))?;
crate::validate_plan_path(value, limits.max_path_bytes)
.map_err(|error| error.at_field(format!("{parent}.{name}")))?;
}
}
let path_key = path.map(crate::portable_path_key);
let file_key = file.map(crate::portable_path_key);
if path_key.is_some() && file_key.is_some() && path_key != file_key {
return Err(invalid(
"path and legacy file identify different locations",
parent,
));
}
Ok(path_key.or(file_key))
}
fn validate_subject(
subject: &TypedSubject,
parent: &str,
text: &mut TextBudget,
) -> Result<(), PlanError> {
text.code(subject.kind.as_str(), &format!("{parent}.subject.kind"))?;
text.require(&subject.value, &format!("{parent}.subject.value"))
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
struct SubjectIdentity<'a> {
kind: &'a str,
value: &'a str,
}
impl<'a> From<&'a TypedSubject> for SubjectIdentity<'a> {
fn from(value: &'a TypedSubject) -> Self {
Self {
kind: value.kind.as_str(),
value: &value.value,
}
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
struct UncertaintyIdentity<'a> {
location: Option<String>,
line: Option<u32>,
subject: Option<SubjectIdentity<'a>>,
kind: Option<&'a str>,
reason: Option<&'a str>,
excerpt: Option<&'a str>,
}
impl<'a> UncertaintyIdentity<'a> {
fn new(value: &'a UncertainReference, location: Option<String>) -> Self {
Self {
location,
line: value.line,
subject: value.subject.as_ref().map(Into::into),
kind: value.kind.as_ref().map(crate::UncertaintyCode::as_str),
reason: value.reason.as_ref().map(crate::UncertaintyCode::as_str),
excerpt: value.excerpt.as_deref(),
}
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
struct OmissionIdentity<'a> {
location: Option<String>,
subject: Option<SubjectIdentity<'a>>,
operation_index: Option<u32>,
reason: &'a str,
}
impl<'a> OmissionIdentity<'a> {
fn new(value: &'a NotModified, location: Option<String>) -> Self {
Self {
location,
subject: value.subject.as_ref().map(Into::into),
operation_index: value.operation_index,
reason: &value.reason,
}
}
}
fn reject_duplicate<T: Ord>(
seen: &mut BTreeSet<T>,
value: T,
field: &str,
) -> Result<(), PlanError> {
if !seen.insert(value) {
return Err(invalid("duplicate evidence entry", field));
}
Ok(())
}