This repository has been archived on 2026-08-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-retired/_archive/rust-bootstrap/engrams/el-lint/src/lib.rs
T

299 lines
9.9 KiB
Rust

//! el-lint — linter for el source files.
//!
//! Combines:
//! - `el-arch` architectural rule violations (VBD, EBD, swarm, security, graph)
//! - Style checks (naming conventions, function length, empty bodies)
//! - Format check (`el-fmt` canonical check, rule I001)
pub mod error;
pub mod linter;
pub mod report;
pub mod rules;
pub use error::LintError;
pub use linter::Linter;
pub use report::{LintDiagnostic, LintReport, LintSeverity};
/// Lint el source code. Returns a report with all diagnostics.
pub fn lint(source: &str) -> Result<LintReport, LintError> {
Linter::new().lint(source)
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
fn do_lint(src: &str) -> LintReport {
lint(src).unwrap()
}
// 1. Clean, canonical code → no errors, no warnings
#[test]
fn test_clean_code_no_errors() {
let src = "fn add(a: Int, b: Int) -> Int {\n return a + b\n}\n";
let report = do_lint(src);
assert!(!report.has_errors(), "unexpected errors: {:?}", report.diagnostics);
assert_eq!(report.warning_count(), 0, "unexpected warnings: {:?}", report.diagnostics);
}
// 2. @accessor calling @manager fn → VBD-001 error
#[test]
fn test_accessor_calls_manager() {
let src = concat!(
"@manager\nfn save_data() -> Void {\n}\n\n",
"@accessor\nfn get_data() -> Void {\n save_data()\n}\n"
);
let report = do_lint(src);
assert!(
report.has_errors(),
"expected arch error for accessor calling manager"
);
let has_vbd = report
.diagnostics
.iter()
.any(|d| d.code.contains("VBD"));
assert!(has_vbd, "expected VBD code: {:?}", report.diagnostics);
}
// 3. activate in a loop → GRAPH-001 warning
#[test]
fn test_activate_in_loop() {
let src = concat!(
"@accessor\nfn load_all(items: [String]) -> Void {\n",
" for x in items {\n",
" activate User where \"query\"\n",
" }\n}\n"
);
let report = do_lint(src);
let has_graph = report
.diagnostics
.iter()
.any(|d| d.code.contains("GRAPH") || d.code.contains("N1"));
assert!(has_graph, "expected GRAPH/N1 diagnostic: {:?}", report.diagnostics);
}
// 4. Function with uppercase name → S002 warning
#[test]
fn test_fn_uppercase_name() {
let src = "fn MyFunction() -> Void {\n}\n";
let report = do_lint(src);
let has_s002 = report.diagnostics.iter().any(|d| d.code == "S002");
assert!(has_s002, "expected S002: {:?}", report.diagnostics);
}
// 5. Type with lowercase name → S004 warning
#[test]
fn test_type_lowercase_name() {
let src = "type myType {\n x: Int\n}\n";
let report = do_lint(src);
let has_s004 = report.diagnostics.iter().any(|d| d.code == "S004");
assert!(has_s004, "expected S004: {:?}", report.diagnostics);
}
// 6. Empty function body → S003 info
#[test]
fn test_empty_fn_body() {
let src = "fn empty() -> Void {\n}\n";
let report = do_lint(src);
let has_s003 = report.diagnostics.iter().any(|d| d.code == "S003");
assert!(has_s003, "expected S003: {:?}", report.diagnostics);
}
// 7. Non-canonical formatting → I001 info
#[test]
fn test_non_canonical_format() {
// Missing trailing newline triggers I001 (formatter adds it, source doesn't have it)
let src = "42";
let report = do_lint(src);
let has_i001 = report.diagnostics.iter().any(|d| d.code == "I001");
assert!(has_i001, "expected I001: {:?}", report.diagnostics);
}
// 8. Canonical formatting → no I001
#[test]
fn test_canonical_format_no_i001() {
let src = "fn add(a: Int, b: Int) -> Int {\n return a + b\n}\n";
let report = do_lint(src);
let has_i001 = report.diagnostics.iter().any(|d| d.code == "I001");
assert!(!has_i001, "unexpected I001: {:?}", report.diagnostics);
}
// 9. has_errors() true when errors present
#[test]
fn test_has_errors_true() {
let src = concat!(
"@manager\nfn save() -> Void {\n}\n\n",
"@accessor\nfn get() -> Void {\n save()\n}\n"
);
let report = do_lint(src);
assert!(report.has_errors());
}
// 10. has_errors() false when only warnings/info
#[test]
fn test_has_errors_false_warnings_only() {
let src = "fn MyFunction() -> Int {\n return 1\n}\n";
let report = do_lint(src);
assert!(!report.has_errors(), "should not have errors, only warnings");
}
// 11. error_count() correct
#[test]
fn test_error_count() {
let src = concat!(
"@manager\nfn save() -> Void {\n}\n\n",
"@accessor\nfn get() -> Void {\n save()\n}\n"
);
let report = do_lint(src);
assert!(report.error_count() >= 1);
}
// 12. warning_count() correct
#[test]
fn test_warning_count() {
let src = "fn MyFunction() -> Int {\n return 1\n}\n";
let report = do_lint(src);
assert!(report.warning_count() >= 1, "expected at least one warning");
}
// 13. display() output contains "error" prefix for errors
#[test]
fn test_display_error_prefix() {
let src = concat!(
"@manager\nfn save() -> Void {\n}\n\n",
"@accessor\nfn get() -> Void {\n save()\n}\n"
);
let report = do_lint(src);
let display = report.display();
assert!(display.contains("error"), "expected 'error' in display: {display}");
}
// 14. display() contains "No issues found." for clean code
#[test]
fn test_display_no_issues() {
let src = "fn add(a: Int, b: Int) -> Int {\n return a + b\n}\n";
let report = do_lint(src);
if !report.has_errors() && report.warning_count() == 0 {
let display = report.display();
assert!(
display.contains("No issues found."),
"expected 'No issues found.': {display}"
);
}
}
// 15. to_json() is valid JSON
#[test]
fn test_to_json_valid() {
let src = "fn add(a: Int, b: Int) -> Int {\n return a + b\n}\n";
let report = do_lint(src);
let json = report.to_json();
let parsed: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
assert!(parsed.is_array(), "expected JSON array");
}
// 16. to_json() contains severity field
#[test]
fn test_to_json_has_severity() {
let src = "fn MyFunction() -> Int {\n return 1\n}\n";
let report = do_lint(src);
let json = report.to_json();
assert!(json.contains("severity"), "expected severity field: {json}");
}
// 17. Multiple issues in same file → all reported
#[test]
fn test_multiple_issues() {
let src = "fn MyFunction() -> Void {\n}\ntype myType {\n x: Int\n}\n";
let report = do_lint(src);
// S002 for fn name + S003 for empty body + S004 for type name
assert!(
report.diagnostics.len() >= 2,
"expected multiple diagnostics: {:?}",
report.diagnostics
);
}
// 18. @experience calling @experience → arch error
#[test]
fn test_experience_calls_experience() {
let src = concat!(
"@experience\nfn exp_a() -> Void {\n}\n\n",
"@experience\nfn exp_b() -> Void {\n exp_a()\n}\n"
);
let report = do_lint(src);
assert!(
report.has_errors(),
"expected arch error for experience calling experience"
);
}
// 19. @public fn with activate → arch error
#[test]
fn test_public_fn_with_activate() {
let src = "@public\nfn api_fn() -> Void {\n activate User where \"query\"\n}\n";
let report = do_lint(src);
assert!(
report.has_errors(),
"expected arch error for public fn with activate"
);
}
// 20. @swarm_agent calling @swarm_agent → diagnostic
#[test]
fn test_swarm_agent_calls_swarm_agent() {
let src = concat!(
"@swarm_agent\nfn agent_a() -> Void {\n}\n\n",
"@swarm_agent\nfn agent_b() -> Void {\n agent_a()\n}\n"
);
let report = do_lint(src);
// SwarmAgentIsolation should flag this
let has_swarm = report
.diagnostics
.iter()
.any(|d| d.code.contains("SWARM") || d.severity == LintSeverity::Error || d.severity == LintSeverity::Warning);
assert!(has_swarm, "expected swarm diagnostic: {:?}", report.diagnostics);
}
// 21. Nested functions → linting still works
#[test]
fn test_nested_functions() {
let src = concat!(
"fn outer(x: Int) -> Int {\n",
" fn inner(y: Int) -> Int {\n",
" return y + 1\n",
" }\n",
" return inner(x)\n",
"}\n"
);
// Should not panic
let result = lint(src);
assert!(result.is_ok(), "lint failed on nested functions");
}
// 22. LintReport::file_path is None by default
#[test]
fn test_file_path_none() {
let src = "fn f() -> Void {\n}\n";
let report = do_lint(src);
assert!(report.file_path.is_none());
}
// 23. source_lines is counted correctly
#[test]
fn test_source_lines_counted() {
let src = "fn f() -> Int {\n return 1\n}\n";
let report = do_lint(src);
assert_eq!(report.source_lines, 3);
}
// 24. Empty source → no crash
#[test]
fn test_empty_source() {
let result = lint("");
assert!(result.is_ok());
}
}