Files
el/engrams/el-lint/src/rules.rs
T
Will Anderson a42429012e rename crates/ to engrams/; add el-compiler el package with bootstrap artifact
- crates/ → engrams/ (Rust engrams live here)
- el-compiler/ added: el self-hosting compiler as an el package
  - src/{compiler,lexer,parser,codegen}.el
  - bootstrap/el-compiler.elc (114KB, Rust-compiled seed)
- el.toml Cargo.toml workspace paths updated
- neuron-rs cross-repo path deps fixed (were pointing to products/ instead of foundation/)
2026-04-29 03:27:32 -05:00

117 lines
3.8 KiB
Rust

//! Style and correctness rules for el-lint (beyond el-arch architectural rules).
use el_parser::{Program, Stmt};
use crate::report::{LintDiagnostic, LintSeverity};
/// Run all style rules against the program and return diagnostics.
pub fn check_style(program: &Program) -> Vec<LintDiagnostic> {
let mut diags = Vec::new();
for stmt in &program.stmts {
check_stmt(stmt, &mut diags);
}
diags
}
fn check_stmt(stmt: &Stmt, diags: &mut Vec<LintDiagnostic>) {
match stmt {
Stmt::FnDef { name, body, .. } => {
// S001: Function body too long (>50 statements)
if body.len() > 50 {
diags.push(LintDiagnostic {
severity: LintSeverity::Warning,
code: "S001".into(),
message: format!(
"function `{name}` has {} statements — consider splitting",
body.len()
),
location: format!("function {name}"),
suggestion: Some(
"extract sub-functions for each logical concern".into(),
),
});
}
// S002: Function name not snake_case
if name.chars().any(|c| c.is_uppercase()) {
diags.push(LintDiagnostic {
severity: LintSeverity::Warning,
code: "S002".into(),
message: format!("function `{name}` should be snake_case"),
location: format!("function {name}"),
suggestion: Some(format!("rename to `{}`", to_snake_case(name))),
});
}
// S003: Empty function body
if body.is_empty() {
diags.push(LintDiagnostic {
severity: LintSeverity::Info,
code: "S003".into(),
message: format!("function `{name}` has an empty body"),
location: format!("function {name}"),
suggestion: Some("add implementation or remove if unused".into()),
});
}
// Recurse into nested function defs
for s in body {
check_stmt(s, diags);
}
}
Stmt::TypeDef { name, .. } => {
// S004: Type name not PascalCase
if !is_pascal_case(name) {
diags.push(LintDiagnostic {
severity: LintSeverity::Warning,
code: "S004".into(),
message: format!("type `{name}` should be PascalCase"),
location: format!("type {name}"),
suggestion: None,
});
}
}
Stmt::EnumDef { name, .. } => {
// S004 also applies to enums
if !is_pascal_case(name) {
diags.push(LintDiagnostic {
severity: LintSeverity::Warning,
code: "S004".into(),
message: format!("enum `{name}` should be PascalCase"),
location: format!("enum {name}"),
suggestion: None,
});
}
}
Stmt::ImplDef { methods, .. } => {
for m in methods {
check_stmt(m, diags);
}
}
_ => {}
}
}
/// Convert CamelCase/mixed to snake_case.
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('_');
}
result.push(c.to_lowercase().next().unwrap());
}
result
}
/// Returns true if the first character is uppercase (PascalCase convention).
fn is_pascal_case(s: &str) -> bool {
s.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
}