#[cfg(test)]
use crate::ast::Statement;
use crate::ast::{Declaration, Gen, Span, Trait};
use crate::sex::context::SexContext;
use crate::sex::tracking::{EffectKind, EffectTracker};
#[derive(Debug, Clone, PartialEq)]
pub enum SexLintError {
SexInPureContext {
effect_kind: EffectKind,
span: Span,
message: String,
},
MutableGlobalOutsideSex {
name: String,
span: Span,
},
FfiOutsideSex {
name: String,
span: Span,
},
IoOutsideSex {
operation: String,
span: Span,
},
}
impl SexLintError {
pub fn code(&self) -> &'static str {
match self {
SexLintError::SexInPureContext { .. } => "E001",
SexLintError::MutableGlobalOutsideSex { .. } => "E002",
SexLintError::FfiOutsideSex { .. } => "E003",
SexLintError::IoOutsideSex { .. } => "E004",
}
}
pub fn span(&self) -> Span {
match self {
SexLintError::SexInPureContext { span, .. }
| SexLintError::MutableGlobalOutsideSex { span, .. }
| SexLintError::FfiOutsideSex { span, .. }
| SexLintError::IoOutsideSex { span, .. } => *span,
}
}
}
impl std::fmt::Display for SexLintError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SexLintError::SexInPureContext {
effect_kind,
span,
message,
} => write!(
f,
"[{}] sex in pure context: {} at line {}, column {} - {}",
self.code(),
effect_kind,
span.line,
span.column,
message
),
SexLintError::MutableGlobalOutsideSex { name, span } => write!(
f,
"[{}] mutable global '{}' accessed outside sex context at line {}, column {}",
self.code(),
name,
span.line,
span.column
),
SexLintError::FfiOutsideSex { name, span } => write!(
f,
"[{}] FFI call '{}' outside sex context at line {}, column {}",
self.code(),
name,
span.line,
span.column
),
SexLintError::IoOutsideSex { operation, span } => write!(
f,
"[{}] I/O operation '{}' outside sex context at line {}, column {}",
self.code(),
operation,
span.line,
span.column
),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum SexLintWarning {
LargeSexBlock {
size: usize,
max_size: usize,
span: Span,
},
SexFunctionWithoutDocumentation {
name: String,
span: Span,
},
}
impl SexLintWarning {
pub fn code(&self) -> &'static str {
match self {
SexLintWarning::LargeSexBlock { .. } => "W001",
SexLintWarning::SexFunctionWithoutDocumentation { .. } => "W002",
}
}
pub fn span(&self) -> Span {
match self {
SexLintWarning::LargeSexBlock { span, .. }
| SexLintWarning::SexFunctionWithoutDocumentation { span, .. } => *span,
}
}
}
impl std::fmt::Display for SexLintWarning {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SexLintWarning::LargeSexBlock {
size,
max_size,
span,
} => write!(
f,
"[{}] large sex block ({} statements, max {}) at line {}, column {}",
self.code(),
size,
max_size,
span.line,
span.column
),
SexLintWarning::SexFunctionWithoutDocumentation { name, span } => write!(
f,
"[{}] sex function '{}' lacks exegesis at line {}, column {}",
self.code(),
name,
span.line,
span.column
),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct LintResult {
pub errors: Vec<SexLintError>,
pub warnings: Vec<SexLintWarning>,
}
impl LintResult {
pub fn new() -> Self {
Self::default()
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
pub fn is_clean(&self) -> bool {
self.errors.is_empty() && self.warnings.is_empty()
}
pub fn add_error(&mut self, error: SexLintError) {
self.errors.push(error);
}
pub fn add_warning(&mut self, warning: SexLintWarning) {
self.warnings.push(warning);
}
}
pub struct SexLinter {
context: SexContext,
max_sex_block_size: usize,
}
impl SexLinter {
pub fn new(context: SexContext) -> Self {
Self {
context,
max_sex_block_size: 50,
}
}
pub fn with_max_block_size(mut self, size: usize) -> Self {
self.max_sex_block_size = size;
self
}
pub fn lint_declaration(&self, decl: &Declaration) -> LintResult {
let mut result = LintResult::new();
let mut tracker = EffectTracker::new();
tracker.track_declaration(decl);
if self.context.is_pure() {
let effects = tracker.get_effects(decl.name());
for effect in effects {
match effect.kind {
EffectKind::Io => {
result.add_error(SexLintError::IoOutsideSex {
operation: effect
.context
.clone()
.unwrap_or_else(|| "unknown".to_string()),
span: effect.span,
});
}
EffectKind::Ffi => {
result.add_error(SexLintError::FfiOutsideSex {
name: effect
.context
.clone()
.unwrap_or_else(|| "unknown".to_string()),
span: effect.span,
});
}
EffectKind::MutableGlobal => {
result.add_error(SexLintError::MutableGlobalOutsideSex {
name: effect
.context
.clone()
.unwrap_or_else(|| "unknown".to_string()),
span: effect.span,
});
}
_ => {
result.add_error(SexLintError::SexInPureContext {
effect_kind: effect.kind,
span: effect.span,
message: effect
.context
.clone()
.unwrap_or_else(|| "unknown effect".to_string()),
});
}
}
}
}
self.check_block_size(decl, &mut result);
if self.context.is_sex() {
self.check_exegesis(decl, &mut result);
}
result
}
fn check_block_size(&self, decl: &Declaration, result: &mut LintResult) {
let (size, span) = match decl {
Declaration::Gene(Gen {
statements, span, ..
})
| Declaration::Trait(Trait {
statements, span, ..
}) => (statements.len(), *span),
_ => return,
};
if size > self.max_sex_block_size {
result.add_warning(SexLintWarning::LargeSexBlock {
size,
max_size: self.max_sex_block_size,
span,
});
}
}
fn check_exegesis(&self, decl: &Declaration, result: &mut LintResult) {
let exegesis = decl.exegesis();
if exegesis.trim().is_empty() || exegesis.trim().len() < 20 {
result.add_warning(SexLintWarning::SexFunctionWithoutDocumentation {
name: decl.name().to_string(),
span: decl.span(),
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Visibility;
#[test]
fn test_lint_error_codes() {
let err = SexLintError::SexInPureContext {
effect_kind: EffectKind::Io,
span: Span::default(),
message: "test".to_string(),
};
assert_eq!(err.code(), "E001");
let err = SexLintError::MutableGlobalOutsideSex {
name: "test".to_string(),
span: Span::default(),
};
assert_eq!(err.code(), "E002");
let err = SexLintError::FfiOutsideSex {
name: "test".to_string(),
span: Span::default(),
};
assert_eq!(err.code(), "E003");
let err = SexLintError::IoOutsideSex {
operation: "test".to_string(),
span: Span::default(),
};
assert_eq!(err.code(), "E004");
}
#[test]
fn test_lint_warning_codes() {
let warn = SexLintWarning::LargeSexBlock {
size: 100,
max_size: 50,
span: Span::default(),
};
assert_eq!(warn.code(), "W001");
let warn = SexLintWarning::SexFunctionWithoutDocumentation {
name: "test".to_string(),
span: Span::default(),
};
assert_eq!(warn.code(), "W002");
}
#[test]
fn test_lint_result() {
let mut result = LintResult::new();
assert!(result.is_clean());
assert!(!result.has_errors());
assert!(!result.has_warnings());
result.add_error(SexLintError::IoOutsideSex {
operation: "test".to_string(),
span: Span::default(),
});
assert!(result.has_errors());
assert!(!result.is_clean());
result.add_warning(SexLintWarning::LargeSexBlock {
size: 100,
max_size: 50,
span: Span::default(),
});
assert!(result.has_warnings());
}
#[test]
fn test_pure_gene_no_effects() {
let linter = SexLinter::new(SexContext::Pure);
let gene = Gen {
visibility: Visibility::default(),
name: "test.gene".to_string(),
extends: None,
statements: vec![Statement::Has {
subject: "test".to_string(),
property: "property".to_string(),
span: Span::default(),
}],
exegesis: "Test gene".to_string(),
span: Span::default(),
};
let result = linter.lint_declaration(&Declaration::Gene(gene));
assert!(result.is_clean());
}
#[test]
fn test_io_in_pure_context() {
let linter = SexLinter::new(SexContext::Pure);
let gene = Gen {
visibility: Visibility::default(),
name: "io.gene".to_string(),
extends: None,
statements: vec![Statement::Has {
subject: "io".to_string(),
property: "file_read".to_string(),
span: Span::default(),
}],
exegesis: "Test gene".to_string(),
span: Span::default(),
};
let result = linter.lint_declaration(&Declaration::Gene(gene));
assert!(result.has_errors());
assert_eq!(result.errors.len(), 1);
assert_eq!(result.errors[0].code(), "E004");
}
#[test]
fn test_large_block_warning() {
let linter = SexLinter::new(SexContext::Pure).with_max_block_size(5);
let statements: Vec<Statement> = (0..10)
.map(|i| Statement::Has {
subject: "test".to_string(),
property: format!("prop{}", i),
span: Span::default(),
})
.collect();
let gene = Gen {
visibility: Visibility::default(),
name: "test.gene".to_string(),
extends: None,
statements,
exegesis: "Test gene".to_string(),
span: Span::default(),
};
let result = linter.lint_declaration(&Declaration::Gene(gene));
assert!(result.has_warnings());
assert_eq!(result.warnings.len(), 1);
assert_eq!(result.warnings[0].code(), "W001");
}
#[test]
fn test_sex_without_documentation() {
let linter = SexLinter::new(SexContext::Sex);
let gene = Gen {
visibility: Visibility::default(),
name: "test.gene".to_string(),
extends: None,
statements: vec![],
exegesis: "Short".to_string(), span: Span::default(),
};
let result = linter.lint_declaration(&Declaration::Gene(gene));
assert!(result.has_warnings());
assert_eq!(result.warnings[0].code(), "W002");
}
}