use super::super::VersioningError;
use super::super::timeline::{VersionId, VersionTimeline};
use crate::error::ParserResult;
use crate::hir::{Annotation, Enumerator, ScopedName};
use crate::lint::AnnotationKind;
pub(super) const UNSUPPORTED_ANNOTATIONS: [&str; 7] = [
"versioned",
"renamedfrom",
"madeoptional",
"maderequired",
"typechangedfrom",
"returntypechangedfrom",
"defaultversion",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Availability {
pub(crate) from: usize,
pub(crate) until: usize,
}
impl Availability {
pub(crate) fn full() -> Self {
Self {
from: 0,
until: usize::MAX,
}
}
pub(crate) fn contains(&self, version: VersionId) -> bool {
version.index() >= self.from && version.index() < self.until
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SiteKind {
Module,
Type,
Member,
Enumerator,
BitmaskValue,
Bitfield,
Interface,
Operation,
Parameter,
Attribute,
Unsupported,
}
impl SiteKind {
fn supports_lifecycle(self) -> bool {
!matches!(self, Self::Unsupported | Self::Bitfield)
}
pub(super) fn describe(self) -> &'static str {
match self {
Self::Module => "module",
Self::Type => "type declaration",
Self::Member => "member",
Self::Enumerator => "enumerator",
Self::BitmaskValue => "bitmask value",
Self::Bitfield => "bitfield",
Self::Interface => "interface",
Self::Operation => "operation",
Self::Parameter => "parameter",
Self::Attribute => "attribute",
Self::Unsupported => "declaration",
}
}
}
#[derive(Debug)]
pub(crate) struct Site<'a> {
pub(crate) path: String,
pub(crate) parent_path: String,
pub(crate) kind: SiteKind,
pub(crate) annotations: &'a [Annotation],
pub(crate) enum_members: Option<&'a [Enumerator]>,
pub(crate) parameter: Option<&'a crate::hir::ParamDcl>,
}
impl<'a> Site<'a> {
pub(crate) fn validate_supported_annotations(&self) -> ParserResult<()> {
for annotation in self.annotations {
if let Some(name) = raw_versioning_name(annotation) {
if UNSUPPORTED_ANNOTATIONS.contains(&name.to_ascii_lowercase().as_str()) {
return Err(VersioningError::UnsupportedVersionAnnotation {
name: format!("@{name}"),
decl: self.path.clone(),
}
.into());
}
}
}
Ok(())
}
pub(crate) fn new(
kind: SiteKind,
path: &str,
parent_path: &str,
annotations: &'a [Annotation],
) -> Self {
Self {
path: path.to_string(),
parent_path: parent_path.to_string(),
kind,
annotations,
enum_members: None,
parameter: None,
}
}
pub(crate) fn with_parameter(mut self, parameter: &'a crate::hir::ParamDcl) -> Self {
self.parameter = Some(parameter);
self
}
pub(crate) fn with_enum_members(mut self, members: &'a [Enumerator]) -> Self {
self.enum_members = Some(members);
self
}
}
pub(crate) trait VersionVisitor {
fn enter(&mut self, site: &Site<'_>, parent: Availability) -> ParserResult<Availability>;
fn walk(
&mut self,
definitions: &[crate::hir::Definition],
path: &str,
parent: Availability,
) -> ParserResult<()>
where
Self: Sized,
{
super::walk::walk(definitions, path, parent, self)
}
}
pub(crate) fn availability(
site: &Site<'_>,
parent: Availability,
timeline: &VersionTimeline,
) -> ParserResult<Availability> {
site.validate_supported_annotations()?;
let mut added: Option<VersionId> = None;
let mut removed: Option<VersionId> = None;
let mut added_name = String::new();
let mut removed_name = String::new();
for annotation in site.annotations {
match annotation {
Annotation::Added { version } => {
if added.is_some() {
return Err(VersioningError::DuplicateLifecycleAnnotation {
annotation: "@added".to_string(),
decl: site.path.clone(),
}
.into());
}
let resolved = resolve_reference(timeline, "@added", &site.path, version)?;
added_name = render_reference(version);
added = Some(resolved);
}
Annotation::Removed { version } => {
if removed.is_some() {
return Err(VersioningError::DuplicateLifecycleAnnotation {
annotation: "@removed".to_string(),
decl: site.path.clone(),
}
.into());
}
let resolved = resolve_reference(timeline, "@removed", &site.path, version)?;
removed_name = render_reference(version);
removed = Some(resolved);
}
Annotation::Version => {}
Annotation::VersionParameter(_) => {}
other => {
let Some(name) = raw_versioning_name(other) else {
continue;
};
if matches!(
AnnotationKind::from_name(name.as_str()),
Some(AnnotationKind::Added | AnnotationKind::Removed)
) {
return Err(VersioningError::MalformedVersionReference {
annotation: format!("@{name}"),
decl: site.path.clone(),
}
.into());
}
if AnnotationKind::from_name(name.as_str()) == Some(AnnotationKind::Version) {
return Err(VersioningError::MalformedVersionMarker {
decl: site.path.clone(),
}
.into());
}
}
}
}
if (added.is_some() || removed.is_some()) && !site.kind.supports_lifecycle() {
let annotation = if added.is_some() {
"@added"
} else {
"@removed"
};
return Err(VersioningError::UnsupportedVersionTarget {
annotation: annotation.to_string(),
target: format!("{} ({})", site.path, site.kind.describe()),
}
.into());
}
let own_from = added.map(VersionId::index).unwrap_or(0);
let own_until = removed.map(VersionId::index).unwrap_or(timeline.len());
if own_from >= own_until {
return Err(VersioningError::EmptyAvailability {
decl: site.path.clone(),
added: added_name,
removed: removed_name,
}
.into());
}
if let Some(version) = added {
if version.index() < parent.from || version.index() >= parent.until {
return Err(VersioningError::ChildOutsideParent {
decl: site.path.clone(),
parent: site.parent_path.clone(),
version: added_name,
}
.into());
}
}
if let Some(version) = removed {
if version.index() >= parent.until || version.index() <= parent.from {
return Err(VersioningError::ChildOutsideParent {
decl: site.path.clone(),
parent: site.parent_path.clone(),
version: removed_name,
}
.into());
}
}
Ok(Availability {
from: own_from.max(parent.from),
until: own_until.min(parent.until),
})
}
pub(crate) fn resolve_reference(
timeline: &VersionTimeline,
annotation: &str,
decl: &str,
reference: &ScopedName,
) -> ParserResult<VersionId> {
timeline.resolve(reference).ok_or_else(|| {
VersioningError::UnknownVersionReference {
annotation: annotation.to_string(),
decl: decl.to_string(),
reference: render_reference(reference),
declared: timeline.declared(),
}
.into()
})
}
pub(crate) fn render_reference(reference: &ScopedName) -> String {
let prefix = if reference.is_root { "::" } else { "" };
format!("{prefix}{}", reference.name.join("::"))
}
pub(super) fn raw_versioning_name(annotation: &Annotation) -> Option<String> {
match annotation {
Annotation::Builtin { name, .. } => Some(name.clone()),
Annotation::ScopedName { name, .. } => name.name.last().cloned(),
_ => None,
}
}
pub(crate) fn is_versioning_annotation(annotation: &Annotation) -> bool {
match annotation {
Annotation::Version
| Annotation::VersionParameter(_)
| Annotation::Added { .. }
| Annotation::Removed { .. } => true,
other => raw_versioning_name(other).is_some_and(|name| {
AnnotationKind::from_name(&name).is_some_and(AnnotationKind::is_lifecycle)
}),
}
}