use std::path::Path;
use std::sync::Arc;
use super::directive::{ARROW, GoDirective};
use super::error::GoProjectError;
use super::replacement::GoReplacementTarget;
use super::{directive, paths};
use crate::observe::{self, Observation};
pub(super) struct ParsedRequirement {
pub(super) path: Box<str>,
pub(super) version: Box<str>,
}
pub(super) struct ParsedExclusion {
pub(super) path: Box<str>,
pub(super) version: Box<str>,
}
pub(super) struct ParsedReplacement {
pub(super) path: Box<str>,
pub(super) version: Option<Box<str>>,
pub(super) target: GoReplacementTarget,
}
pub(super) struct GoManifestDocument {
pub(super) relative: Box<str>,
pub(super) module: Arc<str>,
pub(super) requirements: Box<[ParsedRequirement]>,
pub(super) exclusions: Box<[ParsedExclusion]>,
pub(super) replacements: Box<[ParsedReplacement]>,
pub(super) digest: [u8; 32],
}
#[derive(Default)]
struct ManifestDraft {
module: Option<Arc<str>>,
requirements: Vec<ParsedRequirement>,
exclusions: Vec<ParsedExclusion>,
replacements: Vec<ParsedReplacement>,
}
const IGNORED_VERBS: &[&str] = &["go", "toolchain", "godebug", "retract", "tool", "ignore"];
impl GoManifestDocument {
pub(super) fn read(root: &Path, path: &Path) -> Result<Self, GoProjectError> {
let relative = paths::relative_text(root, path)?;
let text =
std::fs::read_to_string(path).map_err(|source| GoProjectError::ManifestRead {
path: paths::path_text(path),
source,
})?;
observe::record(Observation::ManifestRead(&relative));
interpret(relative, &text)
}
}
fn interpret(relative: Box<str>, text: &str) -> Result<GoManifestDocument, GoProjectError> {
let directives = directive::parse(text).map_err(|error| GoProjectError::ManifestParse {
path: relative.clone(),
line: error.line,
message: error.message,
})?;
let mut draft = ManifestDraft::default();
for entry in directives.iter() {
apply(&mut draft, entry, &relative)?;
}
seal(relative, draft, text)
}
fn apply(
draft: &mut ManifestDraft,
entry: &GoDirective,
manifest: &str,
) -> Result<(), GoProjectError> {
match &*entry.verb {
"module" => set_module(draft, entry, manifest),
"require" => {
draft.requirements.push(parse_requirement(entry, manifest)?);
Ok(())
}
"exclude" => {
draft.exclusions.push(parse_exclusion(entry, manifest)?);
Ok(())
}
"replace" => {
draft.replacements.push(parse_replacement(entry, manifest)?);
Ok(())
}
verb if IGNORED_VERBS.contains(&verb) => Ok(()),
verb => Err(malformed(
manifest,
entry.line,
format!("unknown directive {verb}"),
)),
}
}
fn set_module(
draft: &mut ManifestDraft,
entry: &GoDirective,
manifest: &str,
) -> Result<(), GoProjectError> {
let [declared] = &*entry.arguments else {
return Err(malformed(
manifest,
entry.line,
"a module directive names one module path".to_owned(),
));
};
match draft.module.replace(Arc::from(&**declared)) {
None => Ok(()),
Some(_) => Err(malformed(
manifest,
entry.line,
"the module path is declared more than once".to_owned(),
)),
}
}
fn seal(
relative: Box<str>,
draft: ManifestDraft,
text: &str,
) -> Result<GoManifestDocument, GoProjectError> {
let module = draft
.module
.ok_or_else(|| GoProjectError::MissingModuleDeclaration {
path: relative.clone(),
})?;
let mut requirements = draft.requirements;
let mut exclusions = draft.exclusions;
let mut replacements = draft.replacements;
requirements
.sort_by(|left, right| (&left.path, &left.version).cmp(&(&right.path, &right.version)));
exclusions
.sort_by(|left, right| (&left.path, &left.version).cmp(&(&right.path, &right.version)));
replacements
.sort_by(|left, right| (&left.path, &left.version).cmp(&(&right.path, &right.version)));
check_unique_requirements(&relative, &requirements)?;
check_unique_exclusions(&relative, &exclusions)?;
check_unique_replacements(&relative, &replacements)?;
Ok(GoManifestDocument {
relative,
module,
requirements: requirements.into_boxed_slice(),
exclusions: exclusions.into_boxed_slice(),
replacements: replacements.into_boxed_slice(),
digest: crate::hash::digest_bytes(text.as_bytes()),
})
}
fn check_unique_requirements(
manifest: &str,
requirements: &[ParsedRequirement],
) -> Result<(), GoProjectError> {
let repeated = requirements
.windows(2)
.find(|pair| pair[0].path == pair[1].path);
match repeated {
None => Ok(()),
Some(pair) => Err(GoProjectError::DuplicateRequirement {
path: manifest.into(),
module: pair[0].path.clone(),
}),
}
}
fn check_unique_exclusions(
manifest: &str,
exclusions: &[ParsedExclusion],
) -> Result<(), GoProjectError> {
let repeated = exclusions
.windows(2)
.find(|pair| pair[0].path == pair[1].path && pair[0].version == pair[1].version);
match repeated {
None => Ok(()),
Some(pair) => Err(GoProjectError::DuplicateExclusion {
path: manifest.into(),
module: pair[0].path.clone(),
version: pair[0].version.clone(),
}),
}
}
fn check_unique_replacements(
manifest: &str,
replacements: &[ParsedReplacement],
) -> Result<(), GoProjectError> {
let repeated = replacements
.windows(2)
.find(|pair| pair[0].path == pair[1].path && pair[0].version == pair[1].version);
match repeated {
None => Ok(()),
Some(pair) => Err(GoProjectError::DuplicateReplacement {
path: manifest.into(),
module: pair[0].path.clone(),
version: version_key(pair[0].version.as_deref()),
}),
}
}
fn version_key(version: Option<&str>) -> Box<str> {
match version {
Some(version) => version.into(),
None => Box::from("any version"),
}
}
fn parse_requirement(
entry: &GoDirective,
manifest: &str,
) -> Result<ParsedRequirement, GoProjectError> {
let [path, version] = &*entry.arguments else {
return Err(malformed(
manifest,
entry.line,
"a requirement names one module path and one version".to_owned(),
));
};
Ok(ParsedRequirement {
path: path.clone(),
version: checked_version(manifest, (path, version))?,
})
}
fn parse_exclusion(entry: &GoDirective, manifest: &str) -> Result<ParsedExclusion, GoProjectError> {
let [path, version] = &*entry.arguments else {
return Err(malformed(
manifest,
entry.line,
"an exclusion names one module path and one version".to_owned(),
));
};
Ok(ParsedExclusion {
path: path.clone(),
version: checked_version(manifest, (path, version))?,
})
}
fn parse_replacement(
entry: &GoDirective,
manifest: &str,
) -> Result<ParsedReplacement, GoProjectError> {
let arrow = entry
.arguments
.iter()
.position(|token| &**token == ARROW)
.ok_or_else(|| {
malformed(
manifest,
entry.line,
format!("a replacement separates its sides with {ARROW}"),
)
})?;
let (old, arrowed) = entry.arguments.split_at(arrow);
let new = arrowed.split_first().map(|(_, rest)| rest).unwrap_or(&[]);
let (path, version) = parse_replaced(old, (manifest, entry.line))?;
Ok(ParsedReplacement {
path,
version,
target: parse_target(new, (manifest, entry.line))?,
})
}
fn parse_replaced(
old: &[Box<str>],
context: (&str, u32),
) -> Result<(Box<str>, Option<Box<str>>), GoProjectError> {
let (manifest, line) = context;
match old {
[path] => Ok((path.clone(), None)),
[path, version] => Ok((
path.clone(),
Some(checked_version(manifest, (path, version))?),
)),
_ => Err(malformed(
manifest,
line,
"a replacement replaces one module path".to_owned(),
)),
}
}
fn parse_target(
new: &[Box<str>],
context: (&str, u32),
) -> Result<GoReplacementTarget, GoProjectError> {
let (manifest, line) = context;
match new {
[target] if is_directory_target(target) => Ok(GoReplacementTarget::LocalDirectory(
normalize_target(target),
)),
[_] => Err(malformed(
manifest,
line,
"a replacement without a version must name a directory path".to_owned(),
)),
[target, _] if is_directory_target(target) => Err(malformed(
manifest,
line,
"a directory replacement takes no version".to_owned(),
)),
[target, version] => Ok(GoReplacementTarget::Module {
path: Arc::from(&**target),
version: Arc::from(&*checked_version(manifest, (target, version))?),
}),
_ => Err(malformed(
manifest,
line,
"a replacement names one target".to_owned(),
)),
}
}
fn is_directory_target(target: &str) -> bool {
target.starts_with("./") || target.starts_with("../") || Path::new(target).is_absolute()
}
fn normalize_target(target: &str) -> Box<str> {
target.strip_prefix("./").unwrap_or(target).into()
}
fn checked_version(manifest: &str, declaration: (&str, &str)) -> Result<Box<str>, GoProjectError> {
let (module, version) = declaration;
match is_module_version(version) {
true => Ok(version.into()),
false => Err(GoProjectError::InvalidVersion {
path: manifest.into(),
module: module.into(),
version: version.into(),
}),
}
}
fn is_module_version(version: &str) -> bool {
version
.strip_prefix('v')
.is_some_and(|core| semver::Version::parse(core).is_ok())
}
fn malformed(manifest: &str, line: u32, message: String) -> GoProjectError {
GoProjectError::ManifestParse {
path: manifest.into(),
line,
message: message.into_boxed_str(),
}
}