1use 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
16const AGENT_FILES: &[&str] = &["AGENTS.md", "CLAUDE.md", "GEMINI.md", ".github/copilot-instructions.md"];
19
20#[derive(Debug, Default)]
22pub struct InitReport {
23 pub changed: Vec<PathBuf>,
25 pub notes: Vec<String>,
27}
28
29pub 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(¤t, block);
99 if updated != current {
100 write(&path, &updated, &mut report)?;
101 }
102 }
103 Ok(report)
104}
105
106fn 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
126pub fn recommended_lints() -> &'static str {
128 LINTS.trim_start()
129}
130
131pub 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}