vivacity_core/
phpcs_installer.rs1use crate::error::{Error, Result};
23use crate::layout::Layout;
24use crate::lock::LockPackage;
25use crate::pathutil::{find_shortest_path, normalize_path};
26use serde_json::Value;
27use std::path::{Path, PathBuf};
28
29pub const PLUGIN_NAME: &str = "dealerdirect/phpcodesniffer-composer-installer";
30const PHPCS: &str = "squizlabs/php_codesniffer";
31const STANDARD_TYPE: &str = "phpcodesniffer-standard";
32const VCS_DIRS: &[&str] = &[
33 ".svn",
34 "_svn",
35 "CVS",
36 "_darcs",
37 ".arch-params",
38 ".monotone",
39 ".bzr",
40 ".git",
41 ".hg",
42];
43
44pub fn register_standards(
48 project_dir: &Path,
49 layout: &Layout,
50 packages: &[&LockPackage],
51 root_manifest: &Value,
52) -> Result<()> {
53 let Some(phpcs) = packages.iter().find(|p| p.name() == PHPCS) else {
54 return Ok(());
55 };
56 let Some(phpcs_dir) = layout.abs(phpcs.name()) else {
57 return Ok(());
58 };
59 let cwd = normalize_path(&project_dir.to_string_lossy());
60 let phpcs_path = normalize_path(&phpcs_dir.to_string_lossy());
61 let conf = phpcs_dir.join("CodeSniffer.conf");
62
63 let mut config = read_config(&conf)?;
65 let mut installed: Vec<String> = config
66 .iter()
67 .find(|(k, _)| k == "installed_paths")
68 .map(|(_, v)| v.split(',').map(str::to_owned).collect())
69 .unwrap_or_default();
70 let mut changed = false;
71 installed.retain(|p| {
72 let dir = if crate::pathutil::is_absolute_path(p) {
73 PathBuf::from(p)
74 } else {
75 phpcs_dir.join(p)
76 };
77 let keep = std::fs::canonicalize(&dir).is_ok_and(|d| d.is_dir());
78 if !keep {
79 changed = true;
80 }
81 keep
82 });
83
84 let mut search: Vec<PathBuf> = Vec::new();
86 if root_manifest.get("type").and_then(Value::as_str) == Some(STANDARD_TYPE) {
87 search.push(project_dir.to_path_buf());
88 }
89 for p in packages {
90 if p.package_type() == STANDARD_TYPE {
91 if let Some(dir) = layout.abs(p.name()) {
92 search.push(dir);
93 }
94 }
95 }
96 if !search.is_empty() {
97 let min_depth = if crate::version::normalize_pretty(phpcs.version())
98 .ok()
99 .is_some_and(|v| {
100 crate::version::Version::parse(&v)
101 .is_ok_and(|v| v >= crate::version::Version::parse("3.0.0.0").expect("version"))
102 }) {
103 0
104 } else {
105 1
106 };
107 let max_depth = root_manifest
108 .get("extra")
109 .and_then(|e| e.get("phpcodesniffer-search-depth"))
110 .and_then(Value::as_u64)
111 .map(|d| d as usize)
112 .unwrap_or(3);
113 let mut rulesets: Vec<PathBuf> = Vec::new();
114 for base in &search {
115 find_rulesets(base, 0, min_depth, max_depth, &mut rulesets);
116 }
117 for ruleset in rulesets {
118 let mut standards = normalize_path(&ruleset.to_string_lossy());
119 if standards != cwd {
120 standards = crate::pathutil::php_dirname(&standards);
121 }
122 let relative = find_shortest_path(&phpcs_path, &standards, true);
123 if !installed.contains(&relative) {
124 installed.push(relative);
125 changed = true;
126 }
127 }
128 }
129 if !changed {
130 return Ok(());
131 }
132 config.retain(|(k, _)| k != "installed_paths");
135 if !installed.is_empty() {
136 installed.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
137 config.push(("installed_paths".to_owned(), installed.join(",")));
138 }
139 write_config(&conf, &config)
140}
141
142fn find_rulesets(dir: &Path, depth: usize, min: usize, max: usize, out: &mut Vec<PathBuf>) {
146 let Ok(entries) = std::fs::read_dir(dir) else {
147 return;
148 };
149 let mut names: Vec<std::ffi::OsString> = entries
150 .filter_map(|e| e.ok().map(|e| e.file_name()))
151 .collect();
152 names.sort();
153 for name in names {
154 let path = dir.join(&name);
155 if path.is_dir() {
156 let n = name.to_string_lossy();
157 if VCS_DIRS.contains(&n.as_ref()) {
158 continue;
159 }
160 if depth < max {
161 find_rulesets(&path, depth + 1, min, max, out);
162 }
163 } else if name == "ruleset.xml" && depth >= min && depth <= max && path.is_file() {
164 out.push(dir.to_path_buf());
165 }
166 }
167}
168
169fn read_config(conf: &Path) -> Result<Vec<(String, String)>> {
172 let Ok(text) = std::fs::read_to_string(conf) else {
173 return Ok(Vec::new());
174 };
175 let mut out = Vec::new();
176 let re = regex_lite(&text);
177 for (k, v) in re {
178 out.push((k, v));
179 }
180 Ok(out)
181}
182
183fn regex_lite(text: &str) -> Vec<(String, String)> {
185 let mut out = Vec::new();
186 for line in text.lines() {
187 let line = line.trim();
188 let Some(rest) = line.strip_prefix('\'') else {
189 continue;
190 };
191 let Some((key, rest)) = rest.split_once("' => '") else {
192 continue;
193 };
194 let Some(value) = rest.strip_suffix("',") else {
195 continue;
196 };
197 out.push((
198 key.replace("\\'", "'").replace("\\\\", "\\"),
199 value.replace("\\'", "'").replace("\\\\", "\\"),
200 ));
201 }
202 out
203}
204
205fn write_config(conf: &Path, config: &[(String, String)]) -> Result<()> {
206 let mut map = serde_json::Map::new();
207 for (k, v) in config {
208 map.insert(k.clone(), Value::String(v.clone()));
209 }
210 let exported = crate::runtime_stub::php_var_export(&Value::Object(map), 0);
211 let text = format!("<?php\n $phpCodeSnifferConfig = {exported};\n?>");
212 let tmp = conf.with_extension("conf.vivacity-tmp");
215 std::fs::write(&tmp, text).map_err(Error::io(&tmp))?;
216 std::fs::rename(&tmp, conf).map_err(Error::io(conf))
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn config_round_trip_like_phpcs() {
225 let tmp = tempfile::tempdir().expect("tmp");
226 let conf = tmp.path().join("CodeSniffer.conf");
227 write_config(
228 &conf,
229 &[("installed_paths".into(), "../../a/b,../../c/d".into())],
230 )
231 .unwrap();
232 assert_eq!(
233 std::fs::read_to_string(&conf).unwrap(),
234 "<?php\n $phpCodeSnifferConfig = array (\n 'installed_paths' => '../../a/b,../../c/d',\n);\n?>"
235 );
236 assert_eq!(
237 read_config(&conf).unwrap(),
238 vec![(
239 "installed_paths".to_owned(),
240 "../../a/b,../../c/d".to_owned()
241 )]
242 );
243 }
244
245 #[test]
246 fn rulesets_by_depth() {
247 let tmp = tempfile::tempdir().expect("tmp");
248 let p = tmp.path();
249 std::fs::create_dir_all(p.join("Std/.git")).unwrap();
250 std::fs::write(p.join("ruleset.xml"), "").unwrap(); std::fs::write(p.join("Std/ruleset.xml"), "").unwrap(); std::fs::write(p.join("Std/.git/ruleset.xml"), "").unwrap(); let mut out = Vec::new();
254 find_rulesets(p, 0, 1, 3, &mut out);
255 assert_eq!(out, vec![p.join("Std")]);
256 let mut out = Vec::new();
257 find_rulesets(p, 0, 0, 3, &mut out);
258 assert_eq!(out, vec![p.join("Std"), p.to_path_buf()]);
259 }
260}