//! 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 { 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()); } }