Skip to main content

ravenrs_lint/
init.rs

1//! `cargo ravenrs-lint --init`: the strict rustc and Clippy levels, the test that runs the rules,
2//! and the instructions every coding agent reads.
3
4use std::path::{Path, PathBuf};
5
6use crate::{Error, Language};
7
8const LINTS: &str = include_str!("../templates/lints.toml");
9const CLIPPY: &str = include_str!("../templates/clippy.toml");
10const TEST: &str = include_str!("../templates/test.rs");
11const AGENTS_EN: &str = include_str!("../templates/agents.en.md");
12const AGENTS_PT: &str = include_str!("../templates/agents.pt-BR.md");
13const START: &str = "<!-- ravenrs-lint:start -->";
14const END: &str = "<!-- ravenrs-lint:end -->";
15
16/// Files the coding agents read: Codex and most others read `AGENTS.md`, Claude Code `CLAUDE.md`,
17/// Gemini `GEMINI.md`, GitHub Copilot `.github/copilot-instructions.md`.
18const AGENT_FILES: &[&str] = &["AGENTS.md", "CLAUDE.md", "GEMINI.md", ".github/copilot-instructions.md"];
19
20/// What `init` did.
21#[derive(Debug, Default)]
22pub struct InitReport {
23    /// Files written or changed.
24    pub changed: Vec<PathBuf>,
25    /// Steps left to the person, with why.
26    pub notes: Vec<String>,
27}
28
29/// Prepares the package in `dir` for the strict rules. Running it again updates the agents' block
30/// and leaves the rest as it is.
31pub fn init(dir: &Path, language: Language) -> Result<InitReport, Error> {
32    let mut report = InitReport::default();
33    let manifest_path = dir.join("Cargo.toml");
34    let mut manifest = read(&manifest_path)?;
35    let before = manifest.clone();
36
37    if manifest.contains("[lints]") && manifest.contains("workspace = true") {
38        report.notes.push(
39            "Cargo.toml takes its lints from the workspace: copy the [lints.*] tables from \
40             `cargo ravenrs-lint --print-lints` into [workspace.lints.*] of the root Cargo.toml"
41                .into(),
42        );
43    } else if manifest.contains("[lints.") || manifest.contains("[lints]") {
44        report.notes.push(
45            "Cargo.toml already has [lints]; compare it with `cargo ravenrs-lint --print-lints` and raise \
46             what is lower"
47                .into(),
48        );
49    } else {
50        manifest.push_str(LINTS);
51    }
52
53    if !manifest.contains("ravenrs-lint =") {
54        let line = format!("ravenrs-lint = \"={}\"\n", env!("CARGO_PKG_VERSION"));
55        if let Some(at) = manifest.find("[dev-dependencies]\n") {
56            manifest.insert_str(at + "[dev-dependencies]\n".len(), &line);
57        } else {
58            manifest.push_str(&format!("\n[dev-dependencies]\n{line}"));
59        }
60    }
61
62    if !manifest.contains("metadata.ravenrs-lint]") {
63        let code = match language {
64            Language::English => "en",
65            Language::PortugueseBrazil => "pt-BR",
66        };
67        manifest.push_str(&format!(
68            "\n# Every rule of ravenrs-lint is denied. Lowering one needs a reason:\n\
69             # rule = {{ level = \"warn\", reason = \"...\" }} in [package.metadata.ravenrs-lint.lints].\n\
70             [package.metadata.ravenrs-lint]\nlanguage = \"{code}\"\n"
71        ));
72    }
73    if manifest != before {
74        write(&manifest_path, &manifest, &mut report)?;
75    }
76
77    let clippy = dir.join("clippy.toml");
78    if clippy.exists() {
79        report
80            .notes
81            .push("clippy.toml already exists; compare it with `cargo ravenrs-lint --print-clippy`".into());
82    } else {
83        write(&clippy, CLIPPY, &mut report)?;
84    }
85
86    let test = dir.join("tests").join("ravenrs_lint.rs");
87    if !test.exists() {
88        write(&test, TEST, &mut report)?;
89    }
90
91    let block = match language {
92        Language::English => AGENTS_EN,
93        Language::PortugueseBrazil => AGENTS_PT,
94    };
95    for name in AGENT_FILES {
96        let path = dir.join(name);
97        let current = if path.exists() { read(&path)? } else { String::new() };
98        let updated = with_block(&current, block);
99        if updated != current {
100            write(&path, &updated, &mut report)?;
101        }
102    }
103    Ok(report)
104}
105
106/// The text with the agents' block replaced, or added at the end.
107fn with_block(text: &str, block: &str) -> String {
108    if let (Some(start), Some(end)) = (text.find(START), text.find(END))
109        && start < end
110    {
111        let after = end + END.len();
112        let mut out = String::with_capacity(text.len() + block.len());
113        out.push_str(&text[..start]);
114        out.push_str(block.trim_end());
115        out.push_str(&text[after..]);
116        return out;
117    }
118    let mut out = text.trim_end().to_owned();
119    if !out.is_empty() {
120        out.push_str("\n\n");
121    }
122    out.push_str(block);
123    out
124}
125
126/// The `[lints]` tables `init` writes.
127pub fn recommended_lints() -> &'static str {
128    LINTS.trim_start()
129}
130
131/// The `clippy.toml` `init` writes.
132pub fn recommended_clippy() -> &'static str {
133    CLIPPY
134}
135
136fn read(path: &Path) -> Result<String, Error> {
137    std::fs::read_to_string(path).map_err(|error| Error::Io(path.to_owned(), error))
138}
139
140fn write(path: &Path, text: &str, report: &mut InitReport) -> Result<(), Error> {
141    if let Some(parent) = path.parent() {
142        std::fs::create_dir_all(parent).map_err(|error| Error::Io(parent.to_owned(), error))?;
143    }
144    std::fs::write(path, text).map_err(|error| Error::Io(path.to_owned(), error))?;
145    report.changed.push(path.to_owned());
146    Ok(())
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn the_block_is_replaced_in_place() {
155        let text = format!("# Notes\n\n{START}\nold\n{END}\n\nAfter.\n");
156        let updated = with_block(&text, &format!("{START}\nnew\n{END}\n"));
157        assert_eq!(updated, format!("# Notes\n\n{START}\nnew\n{END}\n\nAfter.\n"));
158    }
159
160    #[test]
161    fn the_block_is_added_once() {
162        let once = with_block("# Notes\n", AGENTS_EN);
163        assert_eq!(with_block(&once, AGENTS_EN), once);
164        assert!(once.starts_with("# Notes\n\n<!--"));
165    }
166}