use crate::ast::*;
use crate::error::{ValidationError, ValidationWarning};
use crate::typechecker::{Type, TypeChecker, TypeError};
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub declaration_name: String,
pub valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationWarning>,
}
impl ValidationResult {
fn new(name: impl Into<String>) -> Self {
Self {
declaration_name: name.into(),
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn is_valid(&self) -> bool {
self.valid && self.errors.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
fn add_error(&mut self, error: ValidationError) {
self.valid = false;
self.errors.push(error);
}
fn add_warning(&mut self, warning: ValidationWarning) {
self.warnings.push(warning);
}
fn add_type_error(&mut self, error: &TypeError, span: Span) {
self.add_error(ValidationError::TypeError {
message: error.message.clone(),
expected: error.expected.as_ref().map(|t| t.to_string()),
actual: error.actual.as_ref().map(|t| t.to_string()),
span,
});
}
}
#[derive(Debug, Clone, Default)]
pub struct ValidationOptions {
pub typecheck: bool,
}
pub fn validate_with_options(decl: &Declaration, options: &ValidationOptions) -> ValidationResult {
let mut result = ValidationResult::new(decl.name());
validate_exegesis(decl, &mut result);
validate_naming(decl, &mut result);
validate_statements(decl, &mut result);
match decl {
Declaration::Gene(gene) => validate_gene(gene, &mut result),
Declaration::Trait(trait_decl) => validate_trait(trait_decl, &mut result),
Declaration::Constraint(constraint) => validate_constraint(constraint, &mut result),
Declaration::System(system) => validate_system(system, &mut result),
Declaration::Evolution(evolution) => validate_evolution(evolution, &mut result),
Declaration::Function(_) => {} Declaration::Const(_) | Declaration::SexVar(_) => {} }
if options.typecheck {
validate_types(decl, &mut result);
}
result
}
pub fn validate(decl: &Declaration) -> ValidationResult {
validate_with_options(decl, &ValidationOptions::default())
}
pub fn validate_file(file: &DolFile) -> FileValidationResult {
validate_file_with_options(file, &ValidationOptions::default())
}
pub fn validate_file_with_options(
file: &DolFile,
options: &ValidationOptions,
) -> FileValidationResult {
let mut result = FileValidationResult::new();
if let Some(ref module) = file.module {
validate_module_decl(module, &mut result);
}
validate_use_declarations(&file.uses, &mut result);
for decl in &file.declarations {
let decl_result = validate_with_options(decl, options);
result.declaration_results.push(decl_result);
}
validate_use_references(file, &mut result);
result
}
#[derive(Debug, Clone)]
pub struct FileValidationResult {
pub module_errors: Vec<ValidationError>,
pub module_warnings: Vec<ValidationWarning>,
pub declaration_results: Vec<ValidationResult>,
}
impl FileValidationResult {
fn new() -> Self {
Self {
module_errors: Vec::new(),
module_warnings: Vec::new(),
declaration_results: Vec::new(),
}
}
pub fn is_valid(&self) -> bool {
self.module_errors.is_empty() && self.declaration_results.iter().all(|r| r.is_valid())
}
pub fn has_warnings(&self) -> bool {
!self.module_warnings.is_empty()
|| self.declaration_results.iter().any(|r| r.has_warnings())
}
pub fn all_errors(&self) -> Vec<&ValidationError> {
let mut errors: Vec<&ValidationError> = self.module_errors.iter().collect();
for result in &self.declaration_results {
errors.extend(result.errors.iter());
}
errors
}
pub fn all_warnings(&self) -> Vec<&ValidationWarning> {
let mut warnings: Vec<&ValidationWarning> = self.module_warnings.iter().collect();
for result in &self.declaration_results {
warnings.extend(result.warnings.iter());
}
warnings
}
fn add_error(&mut self, error: ValidationError) {
self.module_errors.push(error);
}
fn add_warning(&mut self, warning: ValidationWarning) {
self.module_warnings.push(warning);
}
}
fn validate_module_decl(module: &ModuleDecl, result: &mut FileValidationResult) {
if module.path.is_empty() {
result.add_error(ValidationError::InvalidIdentifier {
name: "module".to_string(),
reason: "module path cannot be empty".to_string(),
});
return;
}
for segment in &module.path {
if segment.is_empty() {
result.add_error(ValidationError::InvalidIdentifier {
name: module.path.join("."),
reason: "module path segment cannot be empty".to_string(),
});
} else if !segment.chars().next().unwrap_or('_').is_alphabetic() {
result.add_error(ValidationError::InvalidIdentifier {
name: segment.clone(),
reason: "module path segment must start with a letter".to_string(),
});
}
}
if let Some(ref version) = module.version {
if version.major == 0 && version.minor == 0 && version.patch == 0 {
result.add_warning(ValidationWarning::NamingConvention {
name: module.path.join("."),
suggestion:
"module version 0.0.0 is typically reserved; consider starting at 0.0.1"
.to_string(),
});
}
}
}
fn validate_use_declarations(uses: &[UseDecl], result: &mut FileValidationResult) {
let mut seen_imports: HashSet<String> = HashSet::new();
for use_decl in uses {
let import_key = format_import_key(use_decl);
if seen_imports.contains(&import_key) {
result.add_warning(ValidationWarning::NamingConvention {
name: import_key.clone(),
suggestion: "duplicate import; this import was already declared".to_string(),
});
} else {
seen_imports.insert(import_key);
}
validate_import_path(use_decl, result);
validate_use_visibility(use_decl, result);
}
}
fn format_import_key(use_decl: &UseDecl) -> String {
let source_prefix = match &use_decl.source {
ImportSource::Local => "local:".to_string(),
ImportSource::Registry { org, package, .. } => format!("@{}/{}:", org, package),
ImportSource::Git { url, .. } => format!("git:{}:", url),
ImportSource::Https { url, .. } => format!("https:{}:", url),
};
format!("{}{}", source_prefix, use_decl.path.join("."))
}
fn validate_import_path(use_decl: &UseDecl, result: &mut FileValidationResult) {
if let ImportSource::Registry { org, package, .. } = &use_decl.source {
if org.is_empty() {
result.add_error(ValidationError::InvalidIdentifier {
name: "registry import".to_string(),
reason: "organization name cannot be empty".to_string(),
});
}
if package.is_empty() {
result.add_error(ValidationError::InvalidIdentifier {
name: "registry import".to_string(),
reason: "package name cannot be empty".to_string(),
});
}
}
if let ImportSource::Git { url, .. } = &use_decl.source {
if url.is_empty() {
result.add_error(ValidationError::InvalidIdentifier {
name: "git import".to_string(),
reason: "git URL cannot be empty".to_string(),
});
}
}
if let ImportSource::Https { url, .. } = &use_decl.source {
if url.is_empty() {
result.add_error(ValidationError::InvalidIdentifier {
name: "https import".to_string(),
reason: "URL cannot be empty".to_string(),
});
}
if !url.starts_with("https://") {
result.add_error(ValidationError::InvalidIdentifier {
name: url.clone(),
reason: "HTTPS import URL must start with https://".to_string(),
});
}
}
if let UseItems::Named(items) = &use_decl.items {
let mut seen_names: HashSet<String> = HashSet::new();
for item in items {
if seen_names.contains(&item.name) {
result.add_warning(ValidationWarning::NamingConvention {
name: item.name.clone(),
suggestion: "duplicate item in import list".to_string(),
});
} else {
seen_names.insert(item.name.clone());
}
}
}
}
fn validate_use_visibility(use_decl: &UseDecl, result: &mut FileValidationResult) {
match use_decl.visibility {
Visibility::Public => {
}
Visibility::PubSpirit => {
if !matches!(use_decl.source, ImportSource::Local) {
result.add_warning(ValidationWarning::NamingConvention {
name: format_import_key(use_decl),
suggestion: "pub(spirit) visibility on external imports has limited utility; consider using 'pub' instead".to_string(),
});
}
}
Visibility::PubParent => {
}
Visibility::Private => {
}
}
}
fn validate_use_references(file: &DolFile, _result: &mut FileValidationResult) {
let declared_names: HashSet<String> = file
.declarations
.iter()
.map(|d| d.name().to_string())
.collect();
for use_decl in &file.uses {
if matches!(
use_decl.visibility,
Visibility::Public | Visibility::PubSpirit | Visibility::PubParent
) && matches!(use_decl.source, ImportSource::Local)
{
let full_path = use_decl.path.join(".");
if !full_path.is_empty() && !declared_names.contains(&full_path) {
}
}
}
}
fn validate_exegesis(decl: &Declaration, result: &mut ValidationResult) {
let exegesis = decl.exegesis();
let span = decl.span();
let trimmed_len = exegesis.trim().len();
if trimmed_len < 20 {
result.add_warning(ValidationWarning::ShortExegesis {
length: trimmed_len,
span,
});
}
}
fn validate_naming(decl: &Declaration, result: &mut ValidationResult) {
let name = decl.name();
if name.starts_with('_') {
return;
}
if name.is_empty() {
return;
}
if name.contains('.') {
if !is_valid_qualified_identifier(name) {
result.add_error(ValidationError::InvalidIdentifier {
name: name.to_string(),
reason: "must be a valid qualified identifier (domain.property)".to_string(),
});
}
return;
}
match decl {
Declaration::Gene(_) | Declaration::Trait(_) | Declaration::System(_) => {
if !is_pascal_case(name) && !name.chars().next().is_some_and(|c| c.is_uppercase()) {
result.add_warning(ValidationWarning::NamingConvention {
name: name.to_string(),
suggestion: format!(
"consider using PascalCase for type names: '{}'",
to_pascal_case(name)
),
});
}
}
Declaration::Constraint(_) => {
}
Declaration::Evolution(_) => {}
Declaration::Function(_) => {}
Declaration::Const(_) => {}
Declaration::SexVar(_) => {}
}
}
fn is_pascal_case(s: &str) -> bool {
if s.is_empty() {
return false;
}
let first = s.chars().next().unwrap();
first.is_uppercase() && !s.contains('_')
}
fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect(),
}
})
.collect()
}
fn validate_statements(decl: &Declaration, result: &mut ValidationResult) {
let statements = match decl {
Declaration::Gene(g) => &g.statements,
Declaration::Trait(t) => &t.statements,
Declaration::Constraint(c) => &c.statements,
Declaration::System(s) => &s.statements,
Declaration::Evolution(_)
| Declaration::Function(_)
| Declaration::Const(_)
| Declaration::SexVar(_) => return, };
let mut seen_uses: Vec<&str> = Vec::new();
for stmt in statements {
if let Statement::Uses { reference, .. } = stmt {
if seen_uses.contains(&reference.as_str()) {
result.add_error(ValidationError::DuplicateDefinition {
kind: "uses".to_string(),
name: reference.clone(),
});
} else {
seen_uses.push(reference);
}
}
}
}
fn validate_gene(gene: &Gen, result: &mut ValidationResult) {
for stmt in &gene.statements {
match stmt {
Statement::Has { .. }
| Statement::Is { .. }
| Statement::DerivesFrom { .. }
| Statement::Requires { .. } => {}
Statement::Uses { span, .. } => {
result.add_error(ValidationError::InvalidIdentifier {
name: "uses".to_string(),
reason: "genes cannot use 'uses' statements; use traits instead".to_string(),
});
let _ = span; }
_ => {}
}
}
}
fn validate_trait(trait_decl: &Trait, result: &mut ValidationResult) {
let has_uses = trait_decl
.statements
.iter()
.any(|s| matches!(s, Statement::Uses { .. }));
let has_behavior = trait_decl
.statements
.iter()
.any(|s| matches!(s, Statement::Is { .. }));
if !has_uses && !has_behavior {
result.add_warning(ValidationWarning::NamingConvention {
name: trait_decl.name.clone(),
suggestion: "traits typically include 'uses' or behavior statements".to_string(),
});
}
}
fn validate_constraint(constraint: &Rule, result: &mut ValidationResult) {
let has_constraint_stmts = constraint
.statements
.iter()
.any(|s| matches!(s, Statement::Matches { .. } | Statement::Never { .. }));
if !has_constraint_stmts {
result.add_warning(ValidationWarning::NamingConvention {
name: constraint.name.clone(),
suggestion: "constraints typically include 'matches' or 'never' statements".to_string(),
});
}
}
fn validate_system(system: &System, result: &mut ValidationResult) {
if !is_valid_version(&system.version) {
result.add_error(ValidationError::InvalidVersion {
version: system.version.clone(),
reason: "must be valid semver (X.Y.Z)".to_string(),
});
}
for req in &system.requirements {
if !is_valid_version(&req.version) {
result.add_error(ValidationError::InvalidVersion {
version: req.version.clone(),
reason: format!("invalid version in requirement for '{}'", req.name),
});
}
}
}
fn validate_evolution(evolution: &Evo, result: &mut ValidationResult) {
if !is_valid_version(&evolution.version) {
result.add_error(ValidationError::InvalidVersion {
version: evolution.version.clone(),
reason: "must be valid semver (X.Y.Z)".to_string(),
});
}
if !is_valid_version(&evolution.parent_version) {
result.add_error(ValidationError::InvalidVersion {
version: evolution.parent_version.clone(),
reason: "parent version must be valid semver (X.Y.Z)".to_string(),
});
}
if is_valid_version(&evolution.version)
&& is_valid_version(&evolution.parent_version)
&& !is_version_greater(&evolution.version, &evolution.parent_version)
{
result.add_warning(ValidationWarning::NamingConvention {
name: evolution.name.clone(),
suggestion: format!(
"new version '{}' should be greater than parent '{}'",
evolution.version, evolution.parent_version
),
});
}
if evolution.additions.is_empty()
&& evolution.deprecations.is_empty()
&& evolution.removals.is_empty()
{
result.add_warning(ValidationWarning::NamingConvention {
name: evolution.name.clone(),
suggestion: "evolution should include at least one adds, deprecates, or removes"
.to_string(),
});
}
}
fn validate_types(decl: &Declaration, result: &mut ValidationResult) {
let mut checker = TypeChecker::new();
let span = decl.span();
if let Declaration::Evolution(evolution) = decl {
for stmt in &evolution.additions {
validate_statement_types(stmt, &mut checker, result, span);
}
for stmt in &evolution.deprecations {
validate_statement_types(stmt, &mut checker, result, span);
}
}
for error in checker.errors() {
result.add_type_error(error, span);
}
}
fn validate_statement_types(
_stmt: &Statement,
_checker: &mut TypeChecker,
_result: &mut ValidationResult,
_span: Span,
) {
}
#[allow(dead_code)]
fn validate_expr_types(
expr: &Expr,
checker: &mut TypeChecker,
result: &mut ValidationResult,
span: Span,
) {
if let Err(error) = checker.infer(expr) {
result.add_type_error(&error, span);
}
}
#[allow(dead_code)]
fn validate_stmt_types(
stmt: &Stmt,
checker: &mut TypeChecker,
result: &mut ValidationResult,
span: Span,
) {
match stmt {
Stmt::Let {
name,
type_ann,
value,
} => {
match checker.infer(value) {
Ok(inferred_type) => {
if let Some(ann) = type_ann {
let expected = Type::from_type_expr(ann);
if !types_match(&inferred_type, &expected) {
result.add_type_error(
&TypeError::mismatch(expected, inferred_type),
span,
);
}
}
let _ = name; }
Err(error) => {
result.add_type_error(&error, span);
}
}
}
Stmt::Expr(expr) => {
validate_expr_types(expr, checker, result, span);
}
Stmt::For {
binding: _,
iterable,
body,
} => {
validate_expr_types(iterable, checker, result, span);
for s in body {
validate_stmt_types(s, checker, result, span);
}
}
Stmt::While { condition, body } => {
if let Err(error) = checker.check(condition, &Type::Bool) {
result.add_type_error(&error, span);
}
for s in body {
validate_stmt_types(s, checker, result, span);
}
}
Stmt::Loop { body } => {
for s in body {
validate_stmt_types(s, checker, result, span);
}
}
Stmt::Return(Some(expr)) => {
validate_expr_types(expr, checker, result, span);
}
Stmt::Return(None) | Stmt::Break | Stmt::Continue => {}
Stmt::Assign { target, value } => {
validate_expr_types(target, checker, result, span);
validate_expr_types(value, checker, result, span);
}
}
}
fn types_match(ty1: &Type, ty2: &Type) -> bool {
match (ty1, ty2) {
(Type::Unknown, _) | (_, Type::Unknown) => true,
(Type::Any, _) | (_, Type::Any) => true,
(Type::Error, _) | (_, Type::Error) => true,
(a, b) if a == b => true,
(a, b) if a.is_numeric() && b.is_numeric() => true,
_ => false,
}
}
fn is_valid_qualified_identifier(name: &str) -> bool {
if name.is_empty() {
return false;
}
for part in name.split('.') {
if part.is_empty() {
return false;
}
let mut chars = part.chars();
let first = chars.next().unwrap();
if !first.is_alphabetic() {
return false;
}
for ch in chars {
if !ch.is_alphanumeric() && ch != '_' {
return false;
}
}
}
true
}
fn is_valid_version(version: &str) -> bool {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() != 3 {
return false;
}
for part in parts {
if part.parse::<u64>().is_err() {
return false;
}
}
true
}
fn is_version_greater(version: &str, other: &str) -> bool {
let parse_version = |v: &str| -> (u64, u64, u64) {
let parts: Vec<&str> = v.split('.').collect();
(
parts[0].parse().unwrap_or(0),
parts[1].parse().unwrap_or(0),
parts[2].parse().unwrap_or(0),
)
};
let v1 = parse_version(version);
let v2 = parse_version(other);
v1 > v2
}
#[cfg(test)]
mod tests {
use super::*;
fn make_gene(name: &str, exegesis: &str) -> Declaration {
Declaration::Gene(Gen {
visibility: Visibility::default(),
name: name.to_string(),
extends: None,
statements: vec![Statement::Has {
subject: "test".to_string(),
property: "property".to_string(),
span: Span::default(),
}],
exegesis: exegesis.to_string(),
span: Span::default(),
})
}
#[test]
fn test_valid_declaration() {
let decl = make_gene(
"container.exists",
"A container is the fundamental unit of workload isolation.",
);
let result = validate(&decl);
assert!(result.is_valid());
}
#[test]
fn test_empty_exegesis() {
let decl = make_gene("container.exists", "");
let result = validate(&decl);
assert!(result.is_valid());
assert!(result.has_warnings());
}
#[test]
fn test_short_exegesis_warning() {
let decl = make_gene("container.exists", "Short.");
let result = validate(&decl);
assert!(result.is_valid()); assert!(result.has_warnings());
}
#[test]
fn test_valid_identifier() {
assert!(is_valid_qualified_identifier("container.exists"));
assert!(is_valid_qualified_identifier("identity.cryptographic"));
assert!(is_valid_qualified_identifier("simple"));
assert!(!is_valid_qualified_identifier(""));
assert!(!is_valid_qualified_identifier(".starts.with.dot"));
assert!(!is_valid_qualified_identifier("123invalid"));
}
#[test]
fn test_valid_version() {
assert!(is_valid_version("0.0.1"));
assert!(is_valid_version("1.2.3"));
assert!(is_valid_version("10.20.30"));
assert!(!is_valid_version("1.2"));
assert!(!is_valid_version("1.2.3.4"));
assert!(!is_valid_version("a.b.c"));
}
#[test]
fn test_version_comparison() {
assert!(is_version_greater("0.0.2", "0.0.1"));
assert!(is_version_greater("0.1.0", "0.0.9"));
assert!(is_version_greater("1.0.0", "0.9.9"));
assert!(!is_version_greater("0.0.1", "0.0.2"));
assert!(!is_version_greater("0.0.1", "0.0.1"));
}
#[test]
fn test_validate_with_options_default() {
let decl = make_gene("test.gene", "A test gene for validation options testing.");
let options = ValidationOptions::default();
assert!(!options.typecheck);
let result = validate_with_options(&decl, &options);
assert!(result.is_valid());
}
#[test]
fn test_validate_with_typecheck_enabled() {
let decl = make_gene("test.gene", "A test gene for type checking validation.");
let options = ValidationOptions { typecheck: true };
let result = validate_with_options(&decl, &options);
assert!(result.is_valid());
}
#[test]
fn test_types_match_any() {
assert!(types_match(&Type::Any, &Type::Int32));
assert!(types_match(&Type::String, &Type::Any));
}
#[test]
fn test_types_match_unknown() {
assert!(types_match(&Type::Unknown, &Type::Int32));
assert!(types_match(&Type::String, &Type::Unknown));
}
#[test]
fn test_types_match_error() {
assert!(types_match(&Type::Error, &Type::Int32));
assert!(types_match(&Type::String, &Type::Error));
}
#[test]
fn test_types_match_same() {
assert!(types_match(&Type::Int32, &Type::Int32));
assert!(types_match(&Type::String, &Type::String));
assert!(types_match(&Type::Bool, &Type::Bool));
}
#[test]
fn test_types_match_numeric_promotion() {
assert!(types_match(&Type::Int32, &Type::Int64));
assert!(types_match(&Type::Float32, &Type::Float64));
assert!(types_match(&Type::Int32, &Type::Float64));
}
#[test]
fn test_types_mismatch() {
assert!(!types_match(&Type::String, &Type::Int32));
assert!(!types_match(&Type::Bool, &Type::String));
}
#[test]
fn test_add_type_error_to_result() {
let mut result = ValidationResult::new("test");
let type_error = crate::typechecker::TypeError::mismatch(Type::String, Type::Int32);
result.add_type_error(&type_error, Span::default());
assert!(!result.is_valid());
assert_eq!(result.errors.len(), 1);
match &result.errors[0] {
crate::error::ValidationError::TypeError {
expected, actual, ..
} => {
assert!(expected.as_ref().unwrap().contains("String"));
assert!(actual.as_ref().unwrap().contains("Int32"));
}
_ => panic!("Expected TypeError variant"),
}
}
#[test]
fn test_validation_options_typecheck_flag() {
let options = ValidationOptions { typecheck: true };
assert!(options.typecheck);
let options = ValidationOptions { typecheck: false };
assert!(!options.typecheck);
}
fn make_use_decl(visibility: Visibility, source: ImportSource, path: Vec<&str>) -> UseDecl {
UseDecl {
visibility,
source,
path: path.into_iter().map(|s| s.to_string()).collect(),
items: UseItems::Single,
alias: None,
span: Span::default(),
}
}
#[test]
fn test_validate_file_empty() {
let file = DolFile {
module: None,
uses: vec![],
declarations: vec![],
};
let result = validate_file(&file);
assert!(result.is_valid());
}
#[test]
fn test_validate_file_with_module() {
let file = DolFile {
module: Some(ModuleDecl {
path: vec!["container".to_string()],
version: None,
span: Span::default(),
}),
uses: vec![],
declarations: vec![],
};
let result = validate_file(&file);
assert!(result.is_valid());
}
#[test]
fn test_validate_file_with_versioned_module() {
let file = DolFile {
module: Some(ModuleDecl {
path: vec!["container".to_string(), "lib".to_string()],
version: Some(Version {
major: 1,
minor: 0,
patch: 0,
suffix: None,
}),
span: Span::default(),
}),
uses: vec![],
declarations: vec![],
};
let result = validate_file(&file);
assert!(result.is_valid());
}
#[test]
fn test_validate_file_with_zero_version_warning() {
let file = DolFile {
module: Some(ModuleDecl {
path: vec!["test".to_string()],
version: Some(Version {
major: 0,
minor: 0,
patch: 0,
suffix: None,
}),
span: Span::default(),
}),
uses: vec![],
declarations: vec![],
};
let result = validate_file(&file);
assert!(result.is_valid()); assert!(result.has_warnings());
}
#[test]
fn test_validate_local_use() {
let file = DolFile {
module: None,
uses: vec![make_use_decl(
Visibility::Private,
ImportSource::Local,
vec!["container", "state"],
)],
declarations: vec![],
};
let result = validate_file(&file);
assert!(result.is_valid());
}
#[test]
fn test_validate_pub_use() {
let file = DolFile {
module: None,
uses: vec![make_use_decl(
Visibility::Public,
ImportSource::Local,
vec!["container", "Container"],
)],
declarations: vec![],
};
let result = validate_file(&file);
assert!(result.is_valid());
}
#[test]
fn test_validate_registry_use() {
let file = DolFile {
module: None,
uses: vec![make_use_decl(
Visibility::Private,
ImportSource::Registry {
org: "univrs".to_string(),
package: "std".to_string(),
version: None,
},
vec!["io"],
)],
declarations: vec![],
};
let result = validate_file(&file);
assert!(result.is_valid());
}
#[test]
fn test_validate_duplicate_use_warning() {
let file = DolFile {
module: None,
uses: vec![
make_use_decl(Visibility::Private, ImportSource::Local, vec!["container"]),
make_use_decl(Visibility::Private, ImportSource::Local, vec!["container"]),
],
declarations: vec![],
};
let result = validate_file(&file);
assert!(result.is_valid()); assert!(result.has_warnings());
}
#[test]
fn test_validate_empty_registry_org_error() {
let file = DolFile {
module: None,
uses: vec![make_use_decl(
Visibility::Private,
ImportSource::Registry {
org: "".to_string(),
package: "std".to_string(),
version: None,
},
vec![],
)],
declarations: vec![],
};
let result = validate_file(&file);
assert!(!result.is_valid());
}
#[test]
fn test_validate_https_url_format() {
let file = DolFile {
module: None,
uses: vec![make_use_decl(
Visibility::Private,
ImportSource::Https {
url: "https://example.com/module.dol".to_string(),
sha256: None,
},
vec![],
)],
declarations: vec![],
};
let result = validate_file(&file);
assert!(result.is_valid());
}
#[test]
fn test_validate_https_missing_protocol_error() {
let file = DolFile {
module: None,
uses: vec![make_use_decl(
Visibility::Private,
ImportSource::Https {
url: "example.com/module.dol".to_string(), sha256: None,
},
vec![],
)],
declarations: vec![],
};
let result = validate_file(&file);
assert!(!result.is_valid());
}
#[test]
fn test_file_validation_result_all_errors() {
let mut result = FileValidationResult::new();
result.add_error(ValidationError::InvalidIdentifier {
name: "test".to_string(),
reason: "test error".to_string(),
});
let errors = result.all_errors();
assert_eq!(errors.len(), 1);
}
#[test]
fn test_file_validation_result_all_warnings() {
let mut result = FileValidationResult::new();
result.add_warning(ValidationWarning::ShortExegesis {
length: 5,
span: Span::default(),
});
let warnings = result.all_warnings();
assert_eq!(warnings.len(), 1);
}
}