use std::{fs, io::Error, path::Path};
use just_template::{Template, tmpl};
pub fn gen_mod_file() -> Result<(), Error> {
let root = std::env::current_dir()?;
let lints_dir = root.join("src").join("lints");
let tmpl_file = root.join("tmpls").join("lints.tmpl");
let mod_file = root.join("src").join("lints.rs");
let mut template = Template::from(fs::read_to_string(tmpl_file).unwrap());
let mut entries: Vec<String> = fs::read_dir(&lints_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.filter_map(|e| {
e.path()
.file_stem()
.and_then(|s| s.to_str())
.map(|s| s.to_string())
})
.filter(|name| name != "mod")
.collect();
entries.sort();
for name in &entries {
tmpl!(template, impls {
mod_name = name
})
}
for name in &entries {
tmpl!(template, calls {
name = name
})
}
for name in &entries {
let file_path = lints_dir.join(format!("{name}.rs"));
let has_check_file = fs::read_to_string(&file_path)
.map(|c| c.contains("pub fn check_file("))
.unwrap_or(false);
if has_check_file {
tmpl!(template, file_calls {
name = name
});
tmpl!(template, file_lints {
name = name
});
}
}
fs::write(&mod_file, template.to_string())?;
Ok(())
}
pub fn gen_lint_registry() -> Result<(), Error> {
let root = std::env::current_dir()?;
let lints_dir = root.join("src").join("lints");
let mut lints: Vec<serde_json::Value> = Vec::new();
for entry in fs::read_dir(&lints_dir)? {
let entry = entry?;
let path = entry.path();
if !path.is_file() || path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
let stem = match path.file_stem().and_then(|s| s.to_str()) {
Some(s) => s.to_string(),
None => continue,
};
if stem == "mod" {
continue;
}
let content = fs::read_to_string(&path)?;
if let Some(meta) = parse_lint_file(&content, &stem) {
lints.push(meta);
}
}
lints.sort_by(|a, b| {
a["name"]
.as_str()
.unwrap_or("")
.cmp(b["name"].as_str().unwrap_or(""))
});
let json = serde_json::json!({ "lints": lints });
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR is not set");
let out_path = Path::new(&out_dir).join("registry.json");
fs::write(&out_path, serde_json::to_string_pretty(&json).unwrap())?;
Ok(())
}
fn parse_lint_file(content: &str, name: &str) -> Option<serde_json::Value> {
let mut doc_lines: Vec<String> = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix("//!") {
doc_lines.push(rest.trim().to_string());
} else if !doc_lines.is_empty() {
break;
}
}
let title = doc_lines
.iter()
.find(|l| !l.is_empty())
.cloned()
.unwrap_or_default();
let mut summary = String::new();
let mut in_summary = false;
for line in &doc_lines {
if line.contains("## Summary") {
in_summary = true;
continue;
}
if line.contains("## Metadata") {
in_summary = false;
}
if in_summary && !line.starts_with("## ") {
if !summary.is_empty() {
summary.push('\n');
}
summary.push_str(line.trim());
}
}
let mut author = String::new();
let mut default_level = String::from("warn");
let mut active_on = String::from("File");
let mut in_metadata = false;
for line in &doc_lines {
if line.contains("## Metadata") {
in_metadata = true;
continue;
}
if in_metadata {
if line.starts_with("## ") {
break;
}
if let Some(val) = line
.strip_prefix("Author:")
.or_else(|| line.strip_prefix("Author:"))
{
author = val.trim().trim_matches('`').to_string();
}
if let Some(val) = line
.strip_prefix("Default:")
.or_else(|| line.strip_prefix("Default:"))
{
let raw = val.trim().trim_matches('`');
if raw == "warn" || raw == "allow" || raw == "deny" {
default_level = raw.to_string();
}
}
}
}
if let Some(pos) = content.rfind("pub fn linter(") {
let after = &content[pos..];
if let Some(colon) = after.find(':') {
let type_part = &after[colon + 1..].trim();
let raw = type_part
.trim_start_matches("syn::")
.split([' ', ')', ',', '\n'])
.next()
.unwrap_or("File");
active_on = raw.strip_prefix("Item").unwrap_or(raw).to_string();
}
}
let mut meta = serde_json::Map::new();
meta.insert("author".into(), serde_json::Value::String(author));
meta.insert("default".into(), serde_json::Value::String(default_level));
meta.insert("active_on".into(), serde_json::Value::String(active_on));
Some(serde_json::json!({
"name": name,
"title": title,
"summary": summary.trim(),
"metadata": meta,
}))
}