Skip to content

Commit

Permalink
feat: validate for unique IDs in structs and enums
Browse files Browse the repository at this point in the history
Add the first "compiler" validation logic, which ensures that each ID:
- in named or unnamed fields of a struct or enum variant is unique
- in variants of an enum is unique
  • Loading branch information
dnaka91 committed Oct 20, 2023
1 parent e0fc43e commit 3295f0c
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 18 deletions.
17 changes: 13 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ mimalloc = "0.1.39"
proc-macro2 = { version = "1.0.69", default-features = false }
quote = { version = "1.0.33", default-features = false }
syn = "2.0.38"
thiserror = "1.0.49"
thiserror = "1.0.50"

[profile.release]
lto = true
Expand Down
1 change: 1 addition & 0 deletions crates/stef-build/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ miette.workspace = true
prettyplease = "0.2.15"
proc-macro2.workspace = true
quote.workspace = true
stef-compiler = { path = "../stef-compiler" }
stef-parser = { path = "../stef-parser" }
syn.workspace = true
thiserror.workspace = true
Expand Down
22 changes: 17 additions & 5 deletions crates/stef-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,39 @@ pub enum Error {
},
#[error("failed parsing schema from {file:?}: {message}")]
Parse { message: String, file: PathBuf },
#[error("failed compiling schema from {file:?}")]
Compile {
#[source]
source: stef_compiler::Error,
file: PathBuf,
},
}

pub fn compile(schemas: &[impl AsRef<str>], _includes: &[impl AsRef<Path>]) -> Result<()> {
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap());

for schema in schemas.iter().map(AsRef::as_ref) {
for schema in glob::glob(schema).map_err(|e| Error::Pattern {
source: e,
for schema in glob::glob(schema).map_err(|source| Error::Pattern {
source,
glob: schema.to_owned(),
})? {
let path = schema.map_err(|e| Error::Glob { source: e })?;

let input = std::fs::read_to_string(&path).map_err(|e| Error::Read {
source: e,
let input = std::fs::read_to_string(&path).map_err(|source| Error::Read {
source,
file: path.clone(),
})?;

let schema = Schema::parse(&input).map_err(|e| Error::Parse {
message: e.to_string(),
message: format!("{e:?}"),
file: path.clone(),
})?;

stef_compiler::validate_schema(&schema).map_err(|source| Error::Compile {
source,
file: path.clone(),
})?;

let code = definition::compile_schema(&schema);
let code = prettyplease::unparse(&syn::parse2(code).unwrap());

Expand Down
1 change: 1 addition & 0 deletions crates/stef-compiler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ license.workspace = true

[dependencies]
stef-parser = { path = "../stef-parser" }
thiserror.workspace = true
108 changes: 108 additions & 0 deletions crates/stef-compiler/src/ids.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use std::collections::HashMap;

use stef_parser::{Enum, Fields, Id, Struct};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum DuplicateId {
#[error("duplicate ID in an enum variant")]
EnumVariant(#[from] DuplicateVariantId),
#[error("duplicate ID in a field")]
Field(#[from] DuplicateFieldId),
}

#[derive(Debug, Error)]
#[error("duplicate ID {} in enum variant `{name}`, already used in `{other_name}`", id.0)]
pub struct DuplicateVariantId {
pub id: Id,
pub name: String,
pub other_name: String,
}

#[derive(Debug, Error)]
pub enum DuplicateFieldId {
#[error("duplicate ID {} in field `{name}`, already used in `{other_name}`", id.0)]
Named {
id: Id,
name: String,
other_name: String,
},
#[error("duplicate ID {} in field {position}, already used in {other_position}", id.0)]
Unnamed {
id: Id,
position: usize,
other_position: usize,
},
}

/// Ensure all IDs inside a struct are unique (which are the field IDs).
pub(crate) fn validate_struct_ids(value: &Struct<'_>) -> Result<(), DuplicateFieldId> {
validate_field_ids(&value.fields)
}

/// Ensure all IDs inside an enum are unique, which means all variants have a unique ID, plus all
/// potential fields in a variant are unique (within that variant).
pub(crate) fn validate_enum_ids(value: &Enum<'_>) -> Result<(), DuplicateId> {
let mut visited = HashMap::with_capacity(value.variants.len());
value
.variants
.iter()
.find_map(|variant| {
visited
.insert(variant.id, variant.name)
.map(|other_name| {
DuplicateVariantId {
id: variant.id,
name: variant.name.to_owned(),
other_name: other_name.to_owned(),
}
.into()
})
.or_else(|| {
validate_field_ids(&variant.fields)
.err()
.map(DuplicateId::from)
})
})
.map_or(Ok(()), Err)
}

/// Ensure all field IDs of a struct or enum are unique.
fn validate_field_ids(value: &Fields) -> Result<(), DuplicateFieldId> {
match value {
Fields::Named(named) => {
let mut visited = HashMap::with_capacity(named.len());
named
.iter()
.find_map(|field| {
visited.insert(field.id, field.name).map(|other_field| {
DuplicateFieldId::Named {
id: field.id,
name: field.name.to_owned(),
other_name: other_field.to_owned(),
}
})
})
.map_or(Ok(()), Err)?;
}
Fields::Unnamed(unnamed) => {
let mut visited = HashMap::with_capacity(unnamed.len());
unnamed
.iter()
.enumerate()
.find_map(|(pos, field)| {
visited
.insert(field.id, pos)
.map(|other_position| DuplicateFieldId::Unnamed {
id: field.id,
position: pos,
other_position,
})
})
.map_or(Ok(()), Err)?;
}
Fields::Unit => {}
}

Ok(())
}
29 changes: 29 additions & 0 deletions crates/stef-compiler/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
pub use ids::{DuplicateFieldId, DuplicateId, DuplicateVariantId};
use stef_parser::{Definition, Schema};
use thiserror::Error;

mod ids;

#[derive(Debug, Error)]
pub enum Error {
#[error("duplicate ID found")]
DuplicateId(#[from] DuplicateId),
}

impl From<DuplicateFieldId> for Error {
fn from(v: DuplicateFieldId) -> Self {
Self::DuplicateId(v.into())
}
}

pub fn validate_schema(value: &Schema<'_>) -> Result<(), Error> {
value.definitions.iter().try_for_each(validate_definition)
}

fn validate_definition(value: &Definition<'_>) -> Result<(), Error> {
match value {
Definition::Module(m) => m.definitions.iter().try_for_each(validate_definition),
Definition::Struct(s) => ids::validate_struct_ids(s).map_err(Into::into),
Definition::Enum(e) => ids::validate_enum_ids(e).map_err(Into::into),
Definition::TypeAlias(_) | Definition::Const(_) | Definition::Import(_) => Ok(()),
}
}
2 changes: 1 addition & 1 deletion crates/stef-parser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ rustc-args = ["--cfg", "docsrs"]
miette.workspace = true
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
stef-derive = { path = "../stef-derive" }
winnow = "0.5.16"
winnow = "0.5.17"

[dev-dependencies]
indoc.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion crates/stef-parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -763,7 +763,7 @@ impl<'a> Display for Generics<'a> {
/// ```txt
/// @1
/// ```
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Id(pub u32);

impl Display for Id {
Expand Down
1 change: 1 addition & 0 deletions crates/stef-playground/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ publish = false
stef = { path = "../stef" }

[build-dependencies]
anyhow = "1.0.75"
stef-build = { path = "../stef-build" }
7 changes: 4 additions & 3 deletions crates/stef-playground/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
fn main() {
stef_build::compile(&["src/sample.stef"], &["src/"]).unwrap();
stef_build::compile(&["schemas/*.stef"], &["schemas/"]).unwrap();
fn main() -> anyhow::Result<()> {
stef_build::compile(&["src/sample.stef"], &["src/"])?;
stef_build::compile(&["schemas/*.stef"], &["schemas/"])?;
Ok(())
}
6 changes: 3 additions & 3 deletions crates/stef/src/buf/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;

#[derive(Debug)]
pub enum Error {
InsufficentData,
InsufficientData,
DecodeInt(varint::DecodeIntError),
NonUtf8(std::string::FromUtf8Error),
MissingField { id: u32, name: Option<&'static str> },
Expand All @@ -39,7 +39,7 @@ pub const END_MARKER: u32 = 0;
macro_rules! ensure_size {
($r:ident, $size:expr) => {
if $r.remaining() < $size {
return Err(Error::InsufficentData);
return Err(Error::InsufficientData);
}
};
}
Expand Down Expand Up @@ -155,7 +155,7 @@ where
{
let len = decode_u64(r)?;
if (len as usize) < N {
return Err(Error::InsufficentData);
return Err(Error::InsufficientData);
}

let buf = (0..N).map(|_| decode(r)).collect::<Result<Vec<_>>>()?;
Expand Down

0 comments on commit 3295f0c

Please sign in to comment.