diff --git a/crates/stef-build/src/lib.rs b/crates/stef-build/src/lib.rs index ad81f1e..8dffbed 100644 --- a/crates/stef-build/src/lib.rs +++ b/crates/stef-build/src/lib.rs @@ -9,7 +9,7 @@ use std::{ path::{Path, PathBuf}, }; -use miette::{NamedSource, Report}; +use miette::Report; use stef_parser::Schema; use thiserror::Error; diff --git a/crates/stef-compiler/src/lib.rs b/crates/stef-compiler/src/lib.rs index 364d42d..3dd05fc 100644 --- a/crates/stef-compiler/src/lib.rs +++ b/crates/stef-compiler/src/lib.rs @@ -1,173 +1,31 @@ +//! Compiler for `STEF` schema files. +//! +//! In this context, compiling means to validate schema files and resolve all used types within. +//! This allows any code generator to consider its input valid and not having to do extra validation +//! work on the schemas. +//! +//! # Example +//! +//! Compile a basic `STEF` schema: +//! +//! ``` +//! let schema = stef_parser::Schema::parse("struct Sample(u32 @1)", None).unwrap(); +//! +//! // Ensure the schema in itself is valid (for example no duplicate IDs, type or field names). +//! stef_compiler::validate_schema(&schema).unwrap(); +//! // Resolve all types used in the schema, both in the schema itself and its submodules, and in +//! // potentially types from external schemas that are referenced in it. +//! stef_compiler::resolve_schemas(&[("test", &schema)]).unwrap(); +//! ``` + #![forbid(unsafe_code)] #![deny(rust_2018_idioms, clippy::all)] -#![warn(clippy::pedantic)] +#![warn(missing_docs, clippy::pedantic)] #![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)] -use std::{ - error::Error, - fmt::{self, Display}, -}; - -pub use ids::{DuplicateFieldId, DuplicateId, DuplicateVariantId}; -use miette::{Diagnostic, NamedSource}; -use stef_parser::{Definition, Schema}; -use thiserror::Error; +pub use resolve::schemas as resolve_schemas; +pub use validate::schema as validate_schema; -use self::{ - generics::InvalidGenericType, - names::{DuplicateFieldName, DuplicateName}, - resolve::ResolveError, -}; - -mod generics; mod highlight; -mod ids; -mod names; -mod resolve; - -#[derive(Debug, Diagnostic, Error)] -pub enum ValidationError { - #[error("duplicate ID found")] - #[diagnostic(transparent)] - DuplicateId(#[from] DuplicateId), - #[error("duplicate name found")] - #[diagnostic(transparent)] - DuplicateName(#[from] DuplicateName), - #[error("invalid generic type found")] - #[diagnostic(transparent)] - InvalidGeneric(#[from] InvalidGenericType), -} - -impl From for ValidationError { - fn from(v: DuplicateFieldId) -> Self { - Self::DuplicateId(v.into()) - } -} - -impl From for ValidationError { - fn from(v: DuplicateFieldName) -> Self { - Self::DuplicateName(v.into()) - } -} - -pub fn validate_schema(value: &Schema<'_>) -> Result<(), ValidationError> { - names::validate_names_in_module(&value.definitions)?; - value.definitions.iter().try_for_each(validate_definition) -} - -fn validate_definition(value: &Definition<'_>) -> Result<(), ValidationError> { - match value { - Definition::Module(m) => { - names::validate_names_in_module(&m.definitions)?; - m.definitions.iter().try_for_each(validate_definition)?; - } - 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(_) => {} - } - - Ok(()) -} - -#[derive(Debug)] -pub struct ResolutionError { - source_code: NamedSource, - cause: resolve::ResolveError, -} - -impl Error for ResolutionError { - fn source(&self) -> Option<&(dyn Error + 'static)> { - Some(&self.cause) - } -} - -impl Display for ResolutionError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("type resolution failed") - } -} - -impl Diagnostic for ResolutionError { - fn code<'a>(&'a self) -> Option> { - self.cause.code() - } - - fn severity(&self) -> Option { - self.cause.severity() - } - - fn help<'a>(&'a self) -> Option> { - self.cause.help() - } - - fn url<'a>(&'a self) -> Option> { - self.cause.url() - } - - fn source_code(&self) -> Option<&dyn miette::SourceCode> { - Some(&self.source_code) - } - - fn labels(&self) -> Option + '_>> { - self.cause.labels() - } - - fn related<'a>(&'a self) -> Option + 'a>> { - self.cause.related() - } - - fn diagnostic_source(&self) -> Option<&dyn Diagnostic> { - self.cause.diagnostic_source() - } -} - -pub fn resolve_schemas(values: &[(&str, &Schema<'_>)]) -> Result<(), ResolutionError> { - let modules = values - .iter() - .map(|(name, schema)| (*name, resolve::resolve_types(name, schema))) - .collect::>(); - - for (schema, module) in modules - .iter() - .enumerate() - .map(|(i, (_, module))| (values[i].1, module)) - { - let mut missing = Vec::new(); - resolve::resolve_module_types(module, &mut missing); - - let imports = - resolve::resolve_module_imports(module, &modules).map_err(|e| ResolutionError { - source_code: NamedSource::new( - schema - .path - .as_ref() - .map_or_else(|| "".to_owned(), |p| p.display().to_string()), - schema.source.to_owned(), - ), - cause: ResolveError::Import(e), - })?; - - for ty in missing { - resolve::resolve_type_remotely(ty, &imports).map_err(|e| ResolutionError { - source_code: NamedSource::new( - schema - .path - .as_ref() - .map_or_else(|| "".to_owned(), |p| p.display().to_string()), - schema.source.to_owned(), - ), - cause: e, - })?; - } - } - - Ok(()) -} +pub mod resolve; +pub mod validate; diff --git a/crates/stef-compiler/src/resolve/error.rs b/crates/stef-compiler/src/resolve/error.rs new file mode 100644 index 0000000..5f3d94c --- /dev/null +++ b/crates/stef-compiler/src/resolve/error.rs @@ -0,0 +1,300 @@ +use std::{fmt, fmt::Display, ops::Range}; + +use miette::{Diagnostic, NamedSource}; +use thiserror::Error; + +use crate::highlight; + +/// Reason why type resolution failed. +#[derive(Debug)] +pub struct Error { + pub(super) source_code: NamedSource, + /// Cause of the failure. + pub cause: ResolveError, +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.cause) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("type resolution failed") + } +} + +impl Diagnostic for Error { + fn code<'a>(&'a self) -> Option> { + self.cause.code() + } + + fn severity(&self) -> Option { + self.cause.severity() + } + + fn help<'a>(&'a self) -> Option> { + self.cause.help() + } + + fn url<'a>(&'a self) -> Option> { + self.cause.url() + } + + fn source_code(&self) -> Option<&dyn miette::SourceCode> { + Some(&self.source_code) + } + + fn labels(&self) -> Option + '_>> { + self.cause.labels() + } + + fn related<'a>(&'a self) -> Option + 'a>> { + self.cause.related() + } + + fn diagnostic_source(&self) -> Option<&dyn Diagnostic> { + self.cause.diagnostic_source() + } +} + +/// Specific reason why type resolution failed, split into distinct resolution steps. +#[derive(Debug, Diagnostic, Error)] +pub enum ResolveError { + /// Local type resolution failed. + #[error("failed resolving type in local modules")] + #[diagnostic(transparent)] + Local(#[from] ResolveLocal), + /// Import statement resolution failed. + #[error("failed resolving import statement")] + #[diagnostic(transparent)] + Import(#[from] ResolveImport), + /// Remote (types in another schema) type resolution failed. + #[error("failed resolving type in remote modules")] + #[diagnostic(transparent)] + Remote(#[from] Box), +} + +impl From for ResolveError { + fn from(value: ResolveRemote) -> Self { + Self::Remote(value.into()) + } +} + +/// Failed to resolve the type within a schema's root or one of its submodules. +#[derive(Debug, Diagnostic, Error)] +pub enum ResolveLocal { + /// The referenced submodule doesn't exist. + #[error(transparent)] + #[diagnostic(transparent)] + MissingModule(#[from] MissingModule), + /// The referenced definition in the root or a submodule doesn't exist. + #[error(transparent)] + #[diagnostic(transparent)] + MissingDefinition(#[from] MissingDefinition), + /// The amount of generics between declaration and use side doesn't match. + #[error(transparent)] + #[diagnostic(transparent)] + GenericsCount(#[from] GenericsCount), + /// The referenced definition can't be used as type. + #[error(transparent)] + #[diagnostic(transparent)] + InvalidKind(#[from] InvalidKind), +} + +/// The referenced (sub)module wasn't found in the schema. +#[derive(Debug, Diagnostic, Error)] +#[error("module {} not found", highlight::value(name))] +#[diagnostic(help("the resolution stopped at module path {}", highlight::value(path)))] +pub struct MissingModule { + /// Name of the missing module. + pub name: String, + /// Path of modules at which the resolution failed. + pub path: String, + #[label("used here")] + pub(super) used: Range, +} + +/// The referenced type wasn't found in the schema root or submodule. +#[derive(Debug, Diagnostic, Error)] +#[error( + "definition {} not found in module {}", + highlight::value(name), + highlight::value(path) +)] +pub struct MissingDefinition { + /// Name of the missing type. + pub name: String, + /// Path of the resolved module where resolution failed. + pub path: String, + #[label("used here")] + pub(super) used: Range, +} + +/// The referenced type was found but the amount of generic type parameters didn't match. +#[derive(Debug, Diagnostic, Error)] +#[error( + "the definition has {} generics but the use side has {}", + highlight::value(definition), + highlight::value(usage) +)] +#[diagnostic(help("the amount of generics must always match"))] +pub struct GenericsCount { + /// Amount of generics on the declaration side. + pub definition: usize, + /// Amount of generics on the use side. + pub usage: usize, + #[label("declared here")] + pub(super) declared: Range, + #[label("used here")] + pub(super) used: Range, +} + +/// The referenced definition was found but it's not a type that can be referenced. +#[derive(Debug, Diagnostic, Error)] +#[error( + "definition found, but a {} can't be referenced", + highlight::sample(kind) +)] +#[diagnostic(help("only struct and enum definitions can be used"))] +pub struct InvalidKind { + /// The kind of definition that was found. + pub kind: &'static str, + #[label("declared here")] + pub(super) declared: Range, + #[label("used here")] + pub(super) used: Range, +} + +/// Failed to resolve an import of another schema. +#[derive(Debug, Diagnostic, Error)] +pub enum ResolveImport { + /// The referenced schema doesn't exist. + #[error(transparent)] + #[diagnostic(transparent)] + MissingSchema(#[from] MissingSchema), + /// The referenced module inside the schema doesn't exist. + #[error(transparent)] + #[diagnostic(transparent)] + MissingModule(#[from] MissingModule), + /// The referenced type inside the module deosn't exist. + #[error(transparent)] + #[diagnostic(transparent)] + MissingDefinition(#[from] MissingDefinition), + /// The referenced definition can't be used as type. + #[error(transparent)] + #[diagnostic(transparent)] + InvalidKind(#[from] InvalidKind), +} + +/// The referenced schema wasn't found in the list of available schemas. +#[derive(Debug, Diagnostic, Error)] +#[error("schema {} not found", highlight::value(name))] +pub struct MissingSchema { + /// Name of the missing schema. + pub name: String, + #[label("used here")] + pub(super) used: Range, +} + +/// Failed to resolve a type in another schema. +#[derive(Debug, Diagnostic, Error)] +pub enum ResolveRemote { + /// No matching import for the type exists. + #[error(transparent)] + #[diagnostic(transparent)] + MissingImport(#[from] MissingImport), + /// The referenced module inside the schema doesn't exist. + #[error(transparent)] + #[diagnostic(transparent)] + MissingModule(#[from] MissingModule), + /// The referenced type inside the module deosn't exist. + #[error(transparent)] + #[diagnostic(transparent)] + MissingDefinition(#[from] MissingDefinition), + /// The amount of generics between declaration and use side doesn't match. + #[error(transparent)] + #[diagnostic(transparent)] + GenericsCount(#[from] RemoteGenericsCount), + /// The referenced definition can't be used as type. + #[error(transparent)] + #[diagnostic(transparent)] + InvalidKind(#[from] RemoteInvalidKind), +} + +/// None of the existing imports match for the referenced type. +#[derive(Debug, Diagnostic, Error)] +#[error("missing import for type {}", highlight::value(ty))] +pub struct MissingImport { + /// Name of the type. + pub ty: String, + #[label("used here")] + pub(super) used: Range, +} + +/// Like [`GenericsCount`], the amount of generics between declaration side and use side didn't +/// match, but split into two separate errors to allow error reporting in separate schema files. +#[derive(Debug, Diagnostic, Error)] +#[error( + "the use side has {} generic(s), mismatching with the declaration", + highlight::value(amount) +)] +#[diagnostic(help("the amount of generics must always match"))] +pub struct RemoteGenericsCount { + /// Amount of generics on the use side. + pub amount: usize, + #[label("used here")] + pub(super) used: Range, + /// Error for the declaration side. + #[related] + pub declaration: [RemoteGenericsCountDeclaration; 1], +} + +/// Declaration side error for the [`RemoteGenericsCount`]. +#[derive(Debug, Diagnostic, Error)] +#[error( + "the declaration has {} generic(s), mismatching with the use side", + highlight::value(amount) +)] +pub struct RemoteGenericsCountDeclaration { + /// Amount of generics on the declaration side. + pub amount: usize, + #[source_code] + pub(super) source_code: NamedSource, + #[label("declared here")] + pub(super) used: Range, +} + +/// Like [`InvalidKind`], the referenced definition was found yet is not a type that can be +/// referenced, but split into to separate errors to allow error reporting in separate schema files. +#[derive(Debug, Diagnostic, Error)] +#[error( + "definition found, but a {} can't be referenced", + highlight::sample(kind) +)] +#[diagnostic(help("only struct and enum definitions can be used"))] +pub struct RemoteInvalidKind { + /// The kind of definition that was found. + pub kind: &'static str, + #[label("used here")] + pub(super) used: Range, + /// Error for the declaration side. + #[related] + pub declaration: [RemoteInvalidKindDeclaration; 1], +} + +/// Declaration side error for the [`RemoteInvalidKind`]. +#[derive(Debug, Diagnostic, Error)] +#[error( + "the definition is a {}, which can't be referenced", + highlight::sample(kind) +)] +pub struct RemoteInvalidKindDeclaration { + /// The kind of definition that is declared. + pub kind: &'static str, + #[source_code] + pub(super) source_code: NamedSource, + #[label("declared here")] + pub(super) used: Range, +} diff --git a/crates/stef-compiler/src/resolve.rs b/crates/stef-compiler/src/resolve/mod.rs similarity index 76% rename from crates/stef-compiler/src/resolve.rs rename to crates/stef-compiler/src/resolve/mod.rs index b760b08..de0dac0 100644 --- a/crates/stef-compiler/src/resolve.rs +++ b/crates/stef-compiler/src/resolve/mod.rs @@ -1,99 +1,70 @@ -use std::{collections::HashMap, ops::Range}; +//! Ensure all referenced types within a schema itself, aswell as between schemas exist and are +//! correct. -use miette::{Diagnostic, NamedSource}; +use std::collections::HashMap; + +use miette::NamedSource; use stef_parser::{ DataType, Definition, ExternalType, Fields, Generics, Import, Name, Schema, Spanned, }; -use thiserror::Error; - -use crate::highlight; - -#[derive(Debug, Diagnostic, Error)] -pub enum ResolveError { - #[error("failed resolving type in local modules")] - #[diagnostic(transparent)] - Local(#[from] ResolveLocal), - #[error("failed resolving import statement")] - #[diagnostic(transparent)] - Import(#[from] ResolveImport), - #[error("failed resolving type in remote modules")] - #[diagnostic(transparent)] - Remote(#[from] Box), -} - -impl From for ResolveError { - fn from(value: ResolveRemote) -> Self { - Self::Remote(value.into()) - } -} -#[derive(Debug, Diagnostic, Error)] -pub enum ResolveLocal { - #[error(transparent)] - #[diagnostic(transparent)] - MissingModule(#[from] MissingModule), - #[error(transparent)] - #[diagnostic(transparent)] - MissingDefinition(#[from] MissingDefinition), - #[error(transparent)] - #[diagnostic(transparent)] - GenericsCount(#[from] GenericsCount), - #[error(transparent)] - #[diagnostic(transparent)] - InvalidKind(#[from] InvalidKind), -} +pub use self::error::{ + Error, GenericsCount, InvalidKind, MissingDefinition, MissingImport, MissingModule, + MissingSchema, RemoteGenericsCount, RemoteGenericsCountDeclaration, RemoteInvalidKind, + RemoteInvalidKindDeclaration, ResolveError, ResolveImport, ResolveLocal, ResolveRemote, +}; -#[derive(Debug, Diagnostic, Error)] -#[error("module {} not found", highlight::value(name))] -#[diagnostic(help("the resolution stopped at module path {}", highlight::value(path)))] -pub struct MissingModule { - pub name: String, - pub path: String, - #[label("used here")] - pub used: Range, -} +mod error; -#[derive(Debug, Diagnostic, Error)] -#[error( - "definition {} not found in module {}", - highlight::value(name), - highlight::value(path) -)] -pub struct MissingDefinition { - pub name: String, - pub path: String, - #[label("used here")] - pub used: Range, -} +/// Ensure all referenced types in the schema definitions exist and are valid. +/// +/// This validation happens in three distinct steps: +/// - First, each schema is checked individually, trying to resolve types from submodules. Any +/// not-found types are collected for later checks against external schemas. +/// - Then, the imports in each schema are checked to point to an existing type or module in another +/// schema. +/// - Lastly, the not-found types from the first steps are checked for in the other schemas by +/// utilizing the imports from the second step. +pub fn schemas(values: &[(&str, &Schema<'_>)]) -> Result<(), Error> { + let modules = values + .iter() + .map(|(name, schema)| (*name, resolve_types(name, schema))) + .collect::>(); -#[derive(Debug, Diagnostic, Error)] -#[error( - "the definition has {} generics but the use side has {}", - highlight::value(definition), - highlight::value(usage) -)] -#[diagnostic(help("the amount of generics must always match"))] -pub struct GenericsCount { - pub definition: usize, - pub usage: usize, - #[label("declared here")] - pub declared: Range, - #[label("used here")] - pub used: Range, -} + for (schema, module) in modules + .iter() + .enumerate() + .map(|(i, (_, module))| (values[i].1, module)) + { + let mut missing = Vec::new(); + resolve_module_types(module, &mut missing); + + let imports = resolve_module_imports(module, &modules).map_err(|e| Error { + source_code: NamedSource::new( + schema + .path + .as_ref() + .map_or_else(|| "".to_owned(), |p| p.display().to_string()), + schema.source.to_owned(), + ), + cause: ResolveError::Import(e), + })?; + + for ty in missing { + resolve_type_remotely(ty, &imports).map_err(|e| Error { + source_code: NamedSource::new( + schema + .path + .as_ref() + .map_or_else(|| "".to_owned(), |p| p.display().to_string()), + schema.source.to_owned(), + ), + cause: e, + })?; + } + } -#[derive(Debug, Diagnostic, Error)] -#[error( - "definition found, but a {} can't be referenced", - highlight::sample(kind) -)] -#[diagnostic(help("only struct and enum definitions can be used"))] -pub struct InvalidKind { - pub kind: &'static str, - #[label("declared here")] - pub declared: Range, - #[label("used here")] - pub used: Range, + Ok(()) } pub(crate) struct Module<'a> { @@ -512,25 +483,6 @@ fn visit_externals<'a>(value: &'a DataType<'_>, visit: &mut impl FnMut(&'a Exter } } -#[derive(Debug, Diagnostic, Error)] -pub enum ResolveImport { - #[error("schema {} not found", highlight::value(name))] - MissingSchema { - name: String, - #[label("used here")] - used: Range, - }, - #[error(transparent)] - #[diagnostic(transparent)] - MissingModule(#[from] MissingModule), - #[error(transparent)] - #[diagnostic(transparent)] - MissingDefinition(#[from] MissingDefinition), - #[error(transparent)] - #[diagnostic(transparent)] - InvalidKind(#[from] InvalidKind), -} - pub(crate) fn resolve_module_imports<'a>( module: &Module<'_>, schemas: &'a [(&str, Module<'_>)], @@ -543,7 +495,7 @@ pub(crate) fn resolve_module_imports<'a>( let schema = schemas .iter() .find_map(|(name, schema)| (*name == root.get()).then_some(schema)) - .ok_or_else(|| ResolveImport::MissingSchema { + .ok_or_else(|| MissingSchema { name: root.get().to_owned(), used: root.span().into(), })?; @@ -553,87 +505,6 @@ pub(crate) fn resolve_module_imports<'a>( .collect() } -#[derive(Debug, Diagnostic, Error)] -pub enum ResolveRemote { - #[error(transparent)] - #[diagnostic(transparent)] - MissingImport(#[from] MissingImport), - #[error(transparent)] - #[diagnostic(transparent)] - MissingModule(#[from] MissingModule), - #[error(transparent)] - #[diagnostic(transparent)] - MissingDefinition(#[from] MissingDefinition), - #[error(transparent)] - #[diagnostic(transparent)] - GenericsCount(#[from] RemoteGenericsCount), - #[error(transparent)] - #[diagnostic(transparent)] - InvalidKind(#[from] RemoteInvalidKind), -} - -#[derive(Debug, Diagnostic, Error)] -#[error("missing import for type {}", highlight::value(ty))] -pub struct MissingImport { - ty: String, - #[label("used here")] - used: Range, -} - -#[derive(Debug, Diagnostic, Error)] -#[error( - "the use side has {} generic(s), mismatching with the declaration", - highlight::value(amount) -)] -#[diagnostic(help("the amount of generics must always match"))] -pub struct RemoteGenericsCount { - pub amount: usize, - #[label("used here")] - used: Range, - #[related] - declaration: [RemoteGenericsCountDeclaration; 1], -} - -#[derive(Debug, Diagnostic, Error)] -#[error( - "the declaration has {} generic(s), mismatching with the use side", - highlight::value(amount) -)] -pub struct RemoteGenericsCountDeclaration { - pub amount: usize, - #[source_code] - source_code: NamedSource, - #[label("declared here")] - used: Range, -} - -#[derive(Debug, Diagnostic, Error)] -#[error( - "definition found, but a {} can't be referenced", - highlight::sample(kind) -)] -#[diagnostic(help("only struct and enum definitions can be used"))] -pub struct RemoteInvalidKind { - pub kind: &'static str, - #[label("used here")] - used: Range, - #[related] - pub declaration: [RemoteInvalidKindDeclaration; 1], -} - -#[derive(Debug, Diagnostic, Error)] -#[error( - "the definition is a {}, which can't be referenced", - highlight::sample(kind) -)] -pub struct RemoteInvalidKindDeclaration { - pub kind: &'static str, - #[source_code] - source_code: NamedSource, - #[label("declared here")] - used: Range, -} - pub(crate) fn resolve_type_remotely( ty: LocallyMissingType<'_>, imports: &[ResolvedImport<'_>], diff --git a/crates/stef-compiler/src/generics.rs b/crates/stef-compiler/src/validate/generics.rs similarity index 93% rename from crates/stef-compiler/src/generics.rs rename to crates/stef-compiler/src/validate/generics.rs index 44bb3b1..d69d633 100644 --- a/crates/stef-compiler/src/generics.rs +++ b/crates/stef-compiler/src/validate/generics.rs @@ -4,34 +4,41 @@ use miette::Diagnostic; use stef_parser::{DataType, Enum, ExternalType, Fields, Generics, Span, Spanned, Struct}; use thiserror::Error; +/// Generic type parameters are considered invalid. #[derive(Debug, Diagnostic, Error)] pub enum InvalidGenericType { + /// Two parameters with the same name found. #[error("duplicate generic type name found")] #[diagnostic(transparent)] Duplicate(#[from] DuplicateGenericName), + /// Unused parameter found. #[error("unused generic type argument found")] #[diagnostic(transparent)] Unused(#[from] UnusedGeneric), } +/// Duplicate name for type parameters. #[derive(Debug, Diagnostic, Error)] #[error("duplicate generic type name `{name}`")] #[diagnostic(help("the names of each generic type must be unique"))] pub struct DuplicateGenericName { + /// Name of the parameter. pub name: String, #[label("first declared here")] - pub first: Range, + first: Range, #[label("used here again")] - pub second: Range, + second: Range, } +/// Defined but unused type parameter. #[derive(Debug, Diagnostic, Error)] #[error("unused generic type argument `{name}`")] #[diagnostic(help("each declared generic must be used in some way"))] pub struct UnusedGeneric { + /// Name of the parameter. pub name: String, #[label("declared here")] - pub definition: Range, + definition: Range, } /// Ensure all generics in a struct are unique and used. diff --git a/crates/stef-compiler/src/ids.rs b/crates/stef-compiler/src/validate/ids.rs similarity index 66% rename from crates/stef-compiler/src/ids.rs rename to crates/stef-compiler/src/validate/ids.rs index de39b50..f33116c 100644 --- a/crates/stef-compiler/src/ids.rs +++ b/crates/stef-compiler/src/validate/ids.rs @@ -4,53 +4,81 @@ use miette::Diagnostic; use stef_parser::{Enum, Fields, Id, Spanned, Struct}; use thiserror::Error; +/// Duplicate ID was encountered for two elements in the same scope. #[derive(Debug, Diagnostic, Error)] pub enum DuplicateId { + /// Two enum variants use the same ID. #[error("duplicate ID in an enum variant")] #[diagnostic(transparent)] EnumVariant(#[from] DuplicateVariantId), + /// Two fields use the same ID. #[error("duplicate ID in a field")] #[diagnostic(transparent)] Field(#[from] DuplicateFieldId), } +/// Duplicate ID for enum variants. #[derive(Debug, Diagnostic, Error)] #[error("duplicate ID {} in enum variant `{name}`, already used in `{other_name}`", id.get())] #[diagnostic(help("the IDs for each variant of an enum must be unique"))] pub struct DuplicateVariantId { + /// The duplicate ID. pub id: Id, + /// Name of the variant that tries to use the same ID again. pub name: String, + /// Name of the variant that used the ID for the first time. pub other_name: String, #[label("first declared here")] - pub first: Range, + first: Range, #[label("used here again")] - pub second: Range, + second: Range, } +/// Duplicate ID for fields of a struct or enum variant. #[derive(Debug, Diagnostic, Error)] pub enum DuplicateFieldId { - #[error("duplicate ID {} in field `{name}`, already used in `{other_name}`", id.get())] - #[diagnostic(help("the IDs for each field must be unique"))] - Named { - id: Id, - name: String, - other_name: String, - #[label("first declared here")] - first: Range, - #[label("used here again")] - second: Range, - }, - #[error("duplicate ID {} in position {position}, already used at {other_position}", id.get())] - #[diagnostic(help("the IDs for each field must be unique"))] - Unnamed { - id: Id, - position: usize, - other_position: usize, - #[label("first declared here")] - first: Range, - #[label("used here again")] - second: Range, - }, + /// Found duplicate IDs in named fields. + #[error(transparent)] + #[diagnostic(transparent)] + Named(#[from] DuplicateNamedFieldId), + /// Found duplicate IDs in **un**named fields. + #[error(transparent)] + #[diagnostic(transparent)] + Unnamed(#[from] DuplicateUnnamedFieldId), +} + +/// Duplicate ID for named fields. +#[derive(Debug, Diagnostic, Error)] +#[error("duplicate ID {} in field `{name}`, already used in `{other_name}`", id.get())] +#[diagnostic(help("the IDs for each field must be unique"))] +pub struct DuplicateNamedFieldId { + /// The duplicate ID. + pub id: Id, + /// Name of the field that tries to use the same ID again. + pub name: String, + /// Name of the field that used the ID for the first time. + pub other_name: String, + #[label("first declared here")] + first: Range, + #[label("used here again")] + second: Range, +} + +/// Duplicate ID for unnamed fields. +#[derive(Debug, Diagnostic, Error)] +#[error("duplicate ID {} in position {position}, already used at {other_position}", id.get())] +#[diagnostic(help("the IDs for each field must be unique"))] +pub struct DuplicateUnnamedFieldId { + /// The duplicate ID. + pub id: Id, + /// 1-based position of the field that tries to use the same ID again. + pub position: usize, + /// 1-base position of the field that used the ID for the first time. + pub other_position: usize, + #[label("first declared here")] + first: Range, + #[label("used here again")] + second: Range, } /// Ensure all IDs inside a struct are unique (which are the field IDs). @@ -97,7 +125,7 @@ fn validate_field_ids(value: &Fields<'_>) -> Result<(), DuplicateFieldId> { .find_map(|field| { visited .insert(field.id.get(), (field.name.get(), field.id.span())) - .map(|(other_field, other_span)| DuplicateFieldId::Named { + .map(|(other_field, other_span)| DuplicateNamedFieldId { id: field.id.clone(), name: field.name.get().to_owned(), other_name: other_field.to_owned(), @@ -114,7 +142,7 @@ fn validate_field_ids(value: &Fields<'_>) -> Result<(), DuplicateFieldId> { .enumerate() .find_map(|(pos, field)| { visited.insert(field.id.get(), (pos, field.id.span())).map( - |(other_position, other_span)| DuplicateFieldId::Unnamed { + |(other_position, other_span)| DuplicateUnnamedFieldId { id: field.id.clone(), position: pos + 1, other_position: other_position + 1, diff --git a/crates/stef-compiler/src/validate/mod.rs b/crates/stef-compiler/src/validate/mod.rs new file mode 100644 index 0000000..653f88d --- /dev/null +++ b/crates/stef-compiler/src/validate/mod.rs @@ -0,0 +1,85 @@ +//! Ensure several conditions for a single schema are met, which are difficult to verify during the +//! parsing step. + +use miette::Diagnostic; +use stef_parser::{Definition, Schema}; +use thiserror::Error; + +pub use self::{ + generics::{DuplicateGenericName, InvalidGenericType, UnusedGeneric}, + ids::{ + DuplicateFieldId, DuplicateId, DuplicateNamedFieldId, DuplicateUnnamedFieldId, + DuplicateVariantId, + }, + names::{DuplicateFieldName, DuplicateName, DuplicateNameInModule, DuplicateVariantName}, +}; + +mod generics; +mod ids; +mod names; + +/// Reason why a schema was invalid. +#[derive(Debug, Diagnostic, Error)] +pub enum Error { + /// Duplicate ID was used in a definition. + #[error("duplicate ID found")] + #[diagnostic(transparent)] + DuplicateId(#[from] DuplicateId), + /// Duplicate name was used in a definition, or its name clashes with another one. + #[error("duplicate name found")] + #[diagnostic(transparent)] + DuplicateName(#[from] DuplicateName), + /// Generic type parameters are invalid. + #[error("invalid generic type found")] + #[diagnostic(transparent)] + InvalidGeneric(#[from] InvalidGenericType), +} + +impl From for Error { + fn from(v: DuplicateFieldId) -> Self { + Self::DuplicateId(v.into()) + } +} + +impl From for Error { + fn from(v: DuplicateFieldName) -> Self { + Self::DuplicateName(v.into()) + } +} + +/// Ensure the schema doesn't include invalid definitions, which would be difficult to validate +/// during the parsing step. +/// +/// Currently, it checks that: +/// - All definitions (struct, enums, modules, ...) have a unique name within their module +/// namespace. +/// - IDs in field names or enum variant names are unique. +/// - Fields names in structs or enum variants are unique. +/// - Generic type parameters in a struct or enum are unique. +/// - All generic type parameters are used. +pub fn schema(value: &Schema<'_>) -> Result<(), Error> { + names::validate_names_in_module(&value.definitions)?; + value.definitions.iter().try_for_each(definition) +} + +fn definition(value: &Definition<'_>) -> Result<(), Error> { + match value { + Definition::Module(m) => { + names::validate_names_in_module(&m.definitions)?; + m.definitions.iter().try_for_each(definition)?; + } + 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(_) => {} + } + + Ok(()) +} diff --git a/crates/stef-compiler/src/names.rs b/crates/stef-compiler/src/validate/names.rs similarity index 87% rename from crates/stef-compiler/src/names.rs rename to crates/stef-compiler/src/validate/names.rs index 2418ff4..6a27cc4 100644 --- a/crates/stef-compiler/src/names.rs +++ b/crates/stef-compiler/src/validate/names.rs @@ -4,52 +4,62 @@ use miette::Diagnostic; use stef_parser::{Definition, Enum, Fields, Import, Spanned, Struct}; use thiserror::Error; +/// Duplicate name was encountered for two elements in the same scope. #[derive(Debug, Diagnostic, Error)] pub enum DuplicateName { + /// Two variants of an enum have the same name. #[error("duplicate name in an enum variant")] #[diagnostic(transparent)] EnumVariant(#[from] DuplicateVariantName), + /// Two fields in a struct or enum variant have the same name. #[error("duplicate name in a field")] #[diagnostic(transparent)] Field(#[from] DuplicateFieldName), + /// Two definitions in a module have the same name. #[error("duplicate name in the scope of a module")] #[diagnostic(transparent)] InModule(#[from] DuplicateNameInModule), } +/// Duplicate name for enum variants. #[derive(Debug, Diagnostic, Error)] #[error("duplicate variant name `{name}` in enum")] #[diagnostic(help("the names of each variant must be unique"))] pub struct DuplicateVariantName { + /// Name of the variant. pub name: String, #[label("first declared here")] - pub first: Range, + first: Range, #[label("used here again")] - pub second: Range, + second: Range, } +/// Duplicate name for fields of a struct or enum variant. #[derive(Debug, Diagnostic, Error)] #[error("duplicate field name `{name}`")] #[diagnostic(help("the names of each field must be unique"))] pub struct DuplicateFieldName { + /// Name of the field. pub name: String, #[label("first declared here")] - pub first: Range, + first: Range, #[label("used here again")] - pub second: Range, + second: Range, } +/// Duplicate name for definitions inside a module. #[derive(Debug, Diagnostic, Error)] #[error("duplicate definition name `{name}`")] #[diagnostic(help( "the names of each definition must be unique and not collide with other declarations" ))] pub struct DuplicateNameInModule { + /// Name of the declaration. pub name: String, #[label("first declared here")] - pub first: Range, + first: Range, #[label("used here again")] - pub second: Range, + second: Range, } /// Ensure all field names inside a struct are unique.