Skip to content

Commit

Permalink
feat: verify that generics are unique and used
Browse files Browse the repository at this point in the history
All defined generics in structs or enums must have a unique name and be
used in some way inside the definition of the element. These new
verifications in the compiler ensure the rules are upheld.
  • Loading branch information
dnaka91 committed Oct 24, 2023
1 parent 2737f20 commit f3e0ced
Show file tree
Hide file tree
Showing 17 changed files with 348 additions and 49 deletions.
4 changes: 3 additions & 1 deletion crates/stef-build/src/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,9 @@ fn compile_field_assigns(fields: &Fields<'_>) -> TokenStream {
fn compile_generics(Generics(types): &Generics<'_>) -> (TokenStream, TokenStream) {
(!types.is_empty())
.then(|| {
let types = types.iter().map(|ty| Ident::new(ty, Span::call_site()));
let types = types
.iter()
.map(|ty| Ident::new(ty.get(), Span::call_site()));
let types2 = types.clone();

(
Expand Down
4 changes: 3 additions & 1 deletion crates/stef-build/src/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,9 @@ fn compile_comment(Comment(lines): &Comment<'_>) -> TokenStream {

fn compile_generics(Generics(types): &Generics<'_>) -> Option<TokenStream> {
(!types.is_empty()).then(|| {
let types = types.iter().map(|ty| Ident::new(ty, Span::call_site()));
let types = types
.iter()
.map(|ty| Ident::new(ty.get(), Span::call_site()));
quote! { <#(#types,)*> }
})
}
Expand Down
4 changes: 3 additions & 1 deletion crates/stef-build/src/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,9 @@ fn compile_variant_fields(fields: &Fields<'_>) -> TokenStream {
fn compile_generics(Generics(types): &Generics<'_>) -> (TokenStream, TokenStream) {
(!types.is_empty())
.then(|| {
let types = types.iter().map(|ty| Ident::new(ty, Span::call_site()));
let types = types
.iter()
.map(|ty| Ident::new(ty.get(), Span::call_site()));
let types2 = types.clone();

(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
---
source: crates/stef-build/tests/compiler.rs
expression: "/// Some comment\n struct Sample<\n A,\n B\n > {\n\n a: u32 @1,\n b: bool @2,\n\n }"
expression: "/// Some comment\n struct Sample<\n T\n > {\n\n a: u32 @1,\n b: bool @2,\n c: T @3,\n }"
input_file: crates/stef-parser/tests/inputs/struct-many-ws.stef
---
#[allow(unused_imports)]
use ::stef::buf::{Decode, Encode};
/// Some comment
#[derive(Clone, Debug, PartialEq)]
#[allow(clippy::module_name_repetitions, clippy::option_option)]
pub struct Sample<A, B> {
pub struct Sample<T> {
pub a: u32,
pub b: bool,
pub c: T,
}
#[automatically_derived]
impl<A, B> ::stef::Encode for Sample<A, B>
impl<T> ::stef::Encode for Sample<T>
where
A: ::stef::buf::Encode,
B: ::stef::buf::Encode,
T: ::stef::buf::Encode,
{
#[allow(
clippy::borrow_deref_ref,
Expand All @@ -39,24 +39,32 @@ where
::stef::buf::encode_bool(w, self.b);
},
);
::stef::buf::encode_field(
w,
3,
|w| {
(self.c).encode(w);
},
);
::stef::buf::encode_u32(w, ::stef::buf::END_MARKER);
}
}
#[automatically_derived]
impl<A, B> ::stef::Decode for Sample<A, B>
impl<T> ::stef::Decode for Sample<T>
where
A: ::std::fmt::Debug + ::stef::buf::Decode,
B: ::std::fmt::Debug + ::stef::buf::Decode,
T: ::std::fmt::Debug + ::stef::buf::Decode,
{
#[allow(clippy::type_complexity, clippy::too_many_lines)]
fn decode(r: &mut impl ::stef::Buf) -> ::stef::buf::Result<Self> {
let mut a: Option<u32> = None;
let mut b: Option<bool> = None;
let mut c: Option<T> = None;
loop {
match ::stef::buf::decode_id(r)? {
::stef::buf::END_MARKER => break,
1 => a = Some(::stef::buf::decode_u32(r)?),
2 => b = Some(::stef::buf::decode_bool(r)?),
3 => c = Some(T::decode(r)?),
_ => continue,
}
}
Expand All @@ -71,6 +79,11 @@ where
id: 2,
name: Some("b"),
})?,
c: c
.ok_or(::stef::buf::Error::MissingField {
id: 3,
name: Some("c"),
})?,
})
}
}
Expand Down
172 changes: 172 additions & 0 deletions crates/stef-compiler/src/generics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use std::{collections::HashMap, ops::Range};

use miette::Diagnostic;
use stef_parser::{DataType, Enum, ExternalType, Fields, Generics, Span, Spanned, Struct};
use thiserror::Error;

#[derive(Debug, Diagnostic, Error)]
pub enum InvalidGenericType {
#[error("duplicate generic type name found")]
#[diagnostic(transparent)]
Duplicate(#[from] DuplicateGenericName),
#[error("unused generic type argument found")]
#[diagnostic(transparent)]
Unused(#[from] UnusedGeneric),
}

#[derive(Debug, Diagnostic, Error)]
#[error("duplicate generic type name `{name}`")]
#[diagnostic(help("the names of each generic type must be unique"))]
pub struct DuplicateGenericName {
pub name: String,
#[label("first declared here")]
pub first: Range<usize>,
#[label("used here again")]
pub second: Range<usize>,
}

#[derive(Debug, Diagnostic, Error)]
#[error("unused generic type argument `{name}`")]
#[diagnostic(help("each declared generic must be used in some way"))]
pub struct UnusedGeneric {
pub name: String,
#[label("declared here")]
pub definition: Range<usize>,
}

/// Ensure all generics in a struct are unqiue and used.
pub fn validate_struct_generics(value: &Struct<'_>) -> Result<(), InvalidGenericType> {
validate_duplicate_generics(&value.generics)?;

let mut unvisited = value
.generics
.0
.iter()
.map(|gen| (gen.get(), gen.span()))
.collect::<HashMap<_, _>>();

validate_field_generics(&value.fields, &mut unvisited);

unvisited.into_iter().next().map_or(Ok(()), |(name, span)| {
Err(UnusedGeneric {
name: name.to_owned(),
definition: span.into(),
}
.into())
})
}

/// Ensure all generics in an enum are unqiue and used.
pub fn validate_enum_generics(value: &Enum<'_>) -> Result<(), InvalidGenericType> {
validate_duplicate_generics(&value.generics)?;

let mut unvisited = value
.generics
.0
.iter()
.map(|gen| (gen.get(), gen.span()))
.collect::<HashMap<_, _>>();

for variant in &value.variants {
validate_field_generics(&variant.fields, &mut unvisited);
}

unvisited.into_iter().next().map_or(Ok(()), |(name, span)| {
Err(UnusedGeneric {
name: name.to_owned(),
definition: span.into(),
}
.into())
})
}

/// Ensure all generic type arguments are unique within a struct or enum.
fn validate_duplicate_generics(value: &Generics<'_>) -> Result<(), DuplicateGenericName> {
let mut visited = HashMap::with_capacity(value.0.len());
value
.0
.iter()
.find_map(|name| {
visited
.insert(name.get(), name.span())
.map(|first| DuplicateGenericName {
name: name.get().to_owned(),
first: first.into(),
second: name.span().into(),
})
})
.map_or(Ok(()), Err)
}

/// Iterate over all the fields and mark any generic types as used when disvored as type for a
/// field.
fn validate_field_generics(value: &Fields<'_>, unvisited: &mut HashMap<&str, Span>) {
match &value {
Fields::Named(named) => {
for field in named {
visit_externals(&field.ty, &mut |external| {
if external.path.is_empty() && external.generics.is_empty() {
unvisited.remove(external.name);
}
});
}
}
Fields::Unnamed(unnamed) => {
for field in unnamed {
visit_externals(&field.ty, &mut |external| {
if external.path.is_empty() && external.generics.is_empty() {
unvisited.remove(external.name);
}
});
}
}
Fields::Unit => {}
}
}

/// Iterate recursively through the data type and invoke the closure on each discovered external
/// type.
fn visit_externals(value: &DataType<'_>, visit: &mut impl FnMut(&ExternalType<'_>)) {
match value {
DataType::Bool
| DataType::U8
| DataType::U16
| DataType::U32
| DataType::U64
| DataType::U128
| DataType::I8
| DataType::I16
| DataType::I32
| DataType::I64
| DataType::I128
| DataType::F32
| DataType::F64
| DataType::String
| DataType::StringRef
| DataType::Bytes
| DataType::BytesRef
| DataType::NonZero(_)
| DataType::BoxString
| DataType::BoxBytes => {}
DataType::Vec(ty)
| DataType::HashSet(ty)
| DataType::Option(ty)
| DataType::Array(ty, _) => visit_externals(ty, visit),
DataType::HashMap(kv) => {
visit_externals(&kv.0, visit);
visit_externals(&kv.1, visit);
}
DataType::Tuple(types) => {
for ty in types {
visit_externals(ty, visit);
}
}
DataType::External(ty) => {
visit(ty);

for ty in &ty.generics {
visit_externals(ty, visit);
}
}
}
}
13 changes: 11 additions & 2 deletions crates/stef-compiler/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
#![forbid(unsafe_code)]
#![deny(rust_2018_idioms, clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)]

pub use ids::{DuplicateFieldId, DuplicateId, DuplicateVariantId};
use miette::Diagnostic;
use stef_parser::{Definition, Schema};
use thiserror::Error;

use self::names::{DuplicateFieldName, DuplicateName};
use self::{
generics::InvalidGenericType,
names::{DuplicateFieldName, DuplicateName},
};

mod generics;
mod ids;
mod names;

Expand All @@ -21,6 +25,9 @@ pub enum Error {
#[error("duplicate name found")]
#[diagnostic(transparent)]
DuplicateName(#[from] DuplicateName),
#[error("invalid generic type found")]
#[diagnostic(transparent)]
InvalidGeneric(#[from] InvalidGenericType),
}

impl From<DuplicateFieldId> for Error {
Expand All @@ -47,10 +54,12 @@ fn validate_definition(value: &Definition<'_>) -> Result<(), Error> {
Definition::Struct(s) => {
ids::validate_struct_ids(s)?;
names::validate_struct_names(s)?;
generics::validate_struct_generics(s)?;
}
Definition::Enum(e) => {
ids::validate_enum_ids(e)?;
names::validate_enum_names(e)?;
generics::validate_enum_generics(e)?;
}
Definition::TypeAlias(_) | Definition::Const(_) | Definition::Import(_) => {}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/stef-parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -808,7 +808,7 @@ impl<'a> Display for ExternalType<'a> {
/// <A, B, ...>
/// ```
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Generics<'a>(pub Vec<&'a str>);
pub struct Generics<'a>(pub Vec<Name<'a>>);

impl<'a> Display for Generics<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand Down Expand Up @@ -852,7 +852,7 @@ impl Display for Id {

/// An arbitrary name of any element, which additionally carries a span into the schema to mark its
/// location.
#[derive(Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Name<'a> {
/// Raw string value.
value: &'a str,
Expand Down
9 changes: 7 additions & 2 deletions crates/stef-parser/src/parser/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use winnow::{
};

use super::{ws, Input, Result};
use crate::{highlight, Generics};
use crate::{highlight, Generics, Name};

/// Encountered an invalid `<...>` generics declaration.
#[derive(Debug, ParserError)]
Expand Down Expand Up @@ -62,10 +62,15 @@ pub(super) fn parse<'i>(input: &mut Input<'i>) -> Result<Generics<'i>, ParseErro
})
}

fn parse_name<'i>(input: &mut Input<'i>) -> Result<&'i str, Cause> {
fn parse_name<'i>(input: &mut Input<'i>) -> Result<Name<'i>, Cause> {
(one_of('A'..='Z'), alphanumeric0)
.recognize()
.with_span()
.parse_next(input)
.map(|(name, span)| Name {
value: name,
span: span.into(),
})
.map_err(|e| {
e.map(|()| Cause::InvalidName {
at: input.location(),
Expand Down
5 changes: 2 additions & 3 deletions crates/stef-parser/tests/inputs/struct-many-ws.stef
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@

/// Some comment
struct Sample<
A,
B
T
> {

a: u32 @1,
b: bool @2,

c: T @3,
}
Loading

0 comments on commit f3e0ced

Please sign in to comment.