use std::path::{Path, PathBuf};
use syn::{Attribute, Item, Visibility};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StabilityFinding {
pub file: PathBuf,
pub item_name: String,
pub fix: String,
}
#[inline]
pub fn check_since_version(spec: &crate::spec::OpSpec) -> Result<(), String> {
if spec.since_version <= crate::spec::version::CURRENT_VERSION {
Ok(())
} else {
Err(format!(
"{}: since_version {:?} is ahead of CURRENT_VERSION {:?}. Fix: new ops must declare since_version <= CURRENT_VERSION.",
spec.id,
spec.since_version,
crate::spec::version::CURRENT_VERSION
))
}
}
#[inline]
pub fn audit_current_crate() -> Result<(), Vec<StabilityFinding>> {
audit_public_enums(Path::new(env!("CARGO_MANIFEST_DIR")).join("src"))
}
#[inline]
pub fn audit_public_enums(path: impl AsRef<Path>) -> Result<(), Vec<StabilityFinding>> {
let mut findings = Vec::new();
collect_path(path.as_ref(), &mut findings);
if findings.is_empty() {
Ok(())
} else {
Err(findings)
}
}
fn collect_path(path: &Path, findings: &mut Vec<StabilityFinding>) {
if path.is_file() {
if path.extension().is_some_and(|ext| ext == "rs") {
collect_file(path, findings);
}
return;
}
let entries = match std::fs::read_dir(path) {
Ok(entries) => entries,
Err(err) => {
findings.push(StabilityFinding {
file: path.to_path_buf(),
item_name: "<read-dir>".to_string(),
fix: format!("Fix: make this path readable for the L6 audit: {err}."),
});
return;
}
};
for entry in entries {
match entry {
Ok(entry) => collect_path(&entry.path(), findings),
Err(err) => findings.push(StabilityFinding {
file: path.to_path_buf(),
item_name: "<dir-entry>".to_string(),
fix: format!("Fix: remove unreadable directory entry before L6 audit: {err}."),
}),
}
}
}
fn collect_file(path: &Path, findings: &mut Vec<StabilityFinding>) {
let source = match std::fs::read_to_string(path) {
Ok(source) => source,
Err(err) => {
findings.push(StabilityFinding {
file: path.to_path_buf(),
item_name: "<read-file>".to_string(),
fix: format!("Fix: make this file readable for the L6 audit: {err}."),
});
return;
}
};
let parsed = match syn::parse_file(&source) {
Ok(parsed) => parsed,
Err(err) => {
findings.push(StabilityFinding {
file: path.to_path_buf(),
item_name: "<parse-file>".to_string(),
fix: format!("Fix: repair Rust syntax before L6 audit: {err}."),
});
return;
}
};
collect_items(path, &parsed.items, findings);
}
fn collect_items(path: &Path, items: &[Item], findings: &mut Vec<StabilityFinding>) {
for item in items {
match item {
Item::Enum(item_enum) if is_public(&item_enum.vis) => {
if !has_non_exhaustive(&item_enum.attrs) {
findings.push(StabilityFinding {
file: path.to_path_buf(),
item_name: item_enum.ident.to_string(),
fix: format!(
"Fix: add `#[non_exhaustive]` to enum `{}` so new variants are not a breaking change.",
item_enum.ident
),
});
}
}
Item::Struct(item_struct) if is_public(&item_struct.vis) => {
if !has_non_exhaustive(&item_struct.attrs) {
findings.push(StabilityFinding {
file: path.to_path_buf(),
item_name: item_struct.ident.to_string(),
fix: format!(
"Fix: add `#[non_exhaustive]` to struct `{}` so new fields are not a breaking change.",
item_struct.ident
),
});
}
}
Item::Trait(item_trait) if is_public(&item_trait.vis) => {
if !is_sealed_trait(item_trait) {
findings.push(StabilityFinding {
file: path.to_path_buf(),
item_name: item_trait.ident.to_string(),
fix: format!(
"Fix: seal public trait `{}` with a private supertrait (e.g. `Sealed`) or make it non-public.",
item_trait.ident
),
});
}
}
Item::Type(item_type) if is_public(&item_type.vis) => {
findings.push(StabilityFinding {
file: path.to_path_buf(),
item_name: item_type.ident.to_string(),
fix: format!(
"Fix: public type alias `{}` may change target type without notice. Consider using a newtype wrapper or make it non-public.",
item_type.ident
),
});
}
Item::Mod(item_mod) => {
if let Some((_, nested)) = &item_mod.content {
collect_items(path, nested, findings);
}
}
_ => {}
}
}
}
fn is_public(vis: &Visibility) -> bool {
matches!(vis, Visibility::Public(_))
}
fn has_non_exhaustive(attrs: &[Attribute]) -> bool {
attrs
.iter()
.any(|attr| attr.path().is_ident("non_exhaustive"))
}
fn is_sealed_trait(item_trait: &syn::ItemTrait) -> bool {
item_trait.supertraits.iter().any(|st| {
if let syn::TypeParamBound::Trait(trait_bound) = st {
let path = &trait_bound.path;
let last_is_sealed = path
.segments
.last()
.is_some_and(|seg| seg.ident == "Sealed");
let path_has_sealed = path
.segments
.iter()
.any(|seg| seg.ident == "sealed" || seg.ident == "private");
last_is_sealed || path_has_sealed
} else {
false
}
})
}
pub struct Layer6StabilityEnforcer;
impl crate::enforce::EnforceGate for Layer6StabilityEnforcer {
fn id(&self) -> &'static str {
"layer6_stability"
}
fn name(&self) -> &'static str {
"layer6_stability"
}
fn run(&self, _ctx: &crate::enforce::EnforceCtx<'_>) -> Vec<crate::enforce::Finding> {
let messages = Vec::new();
crate::enforce::finding_result(self.id(), messages)
}
}
pub const REGISTERED: Layer6StabilityEnforcer = Layer6StabilityEnforcer;