git.delta.rocks / jrsonnet / refs/heads / master

difftreelog

source

cmds/jrb/src/rewrite.rs5.7 KiBsourcehistory
1use std::{2	cmp::Reverse,3	fs,4	path::{Path, PathBuf},5};67use jrsonnet_pack::{decode_text, quote_jsonnet};8use jrsonnet_rowan_parser::{AstNode, AstToken, nodes::ExprImport};9use tracing::{debug, error, info, warn};1011#[derive(Debug, Clone)]12pub struct Rule {13	pub legacy: String,14	pub canonical: String,15}1617pub fn rules_from_vendor(vendor_dir: &Path) -> Vec<Rule> {18	let mut rules = Vec::new();19	let entries = match fs::read_dir(vendor_dir) {20		Ok(e) => e,21		Err(e) => {22			warn!("read_dir {}: {e}", vendor_dir.display());23			return rules;24		}25	};26	for entry in entries.flatten() {27		let path = entry.path();28		let Ok(meta) = entry.file_type() else {29			continue;30		};31		if !meta.is_symlink() {32			continue;33		}34		let Some(name) = path.file_name().and_then(|n| n.to_str()) else {35			continue;36		};37		let target = match fs::read_link(&path) {38			Ok(t) => t,39			Err(e) => {40				warn!("readlink {}: {e}", path.display());41				continue;42			}43		};44		let target_str = target.to_string_lossy().replace('\\', "/");45		let canonical = target_str.trim_end_matches('/').to_owned();46		if canonical.is_empty() || canonical.starts_with('/') {47			continue;48		}49		rules.push(Rule {50			legacy: name.to_owned(),51			canonical,52		});53	}54	rules.sort_by_key(|b| Reverse(b.legacy.len()));55	rules56}5758pub struct Stats {59	pub files_scanned: usize,60	pub files_modified: usize,61	pub imports_rewritten: usize,62}6364fn apply_rules(name: &str, rules: &[Rule]) -> Option<String> {65	for r in rules {66		if name == r.legacy {67			return Some(r.canonical.clone());68		}69		if let Some(rest) = name.strip_prefix(&format!("{}/", r.legacy)) {70			return Some(format!("{}/{}", r.canonical, rest));71		}72	}73	None74}7576fn rewrite_source(source: &str, rules: &[Rule]) -> Option<(String, usize)> {77	let (file, errs) = jrsonnet_rowan_parser::parse(source);78	if !errs.is_empty() {79		return None;80	}81	let mut edits: Vec<(usize, usize, String)> = Vec::new();82	for node in file.syntax().descendants() {83		let Some(imp) = ExprImport::cast(node) else {84			continue;85		};86		let Some(text) = imp.text() else { continue };87		let Some(decoded) = decode_text(&text) else {88			continue;89		};90		let Some(new_value) = apply_rules(&decoded, rules) else {91			continue;92		};93		let range = text.syntax().text_range();94		let start: usize = range.start().into();95		let end: usize = range.end().into();96		edits.push((start, end, quote_jsonnet(&new_value)));97	}98	if edits.is_empty() {99		return None;100	}101	edits.sort_by_key(|(s, _, _)| *s);102	let count = edits.len();103	let mut out = String::with_capacity(source.len());104	let mut cursor = 0usize;105	for (start, end, replacement) in edits {106		out.push_str(&source[cursor..start]);107		out.push_str(&replacement);108		cursor = end;109	}110	out.push_str(&source[cursor..]);111	Some((out, count))112}113114fn is_jsonnet_file(path: &Path) -> bool {115	matches!(116		path.extension().and_then(|e| e.to_str()),117		Some("jsonnet" | "libsonnet" | "TEMPLATE")118	)119}120121fn collect_files(root: &Path, skip: &Path, out: &mut Vec<PathBuf>) {122	if root == skip {123		return;124	}125	let meta = match fs::symlink_metadata(root) {126		Ok(m) => m,127		Err(e) => {128			warn!("stat {}: {e}", root.display());129			return;130		}131	};132	let file_type = meta.file_type();133	if file_type.is_symlink() {134		return;135	}136	if file_type.is_dir() {137		let entries = match fs::read_dir(root) {138			Ok(e) => e,139			Err(e) => {140				warn!("read_dir {}: {e}", root.display());141				return;142			}143		};144		for entry in entries.flatten() {145			collect_files(&entry.path(), skip, out);146		}147		return;148	}149	if file_type.is_file() && is_jsonnet_file(root) {150		out.push(root.to_owned());151	}152}153154pub fn rewrite(paths: &[PathBuf], vendor_dir: &Path, rules: &[Rule], dry_run: bool) -> Stats {155	let skip = vendor_dir156		.canonicalize()157		.unwrap_or_else(|_| vendor_dir.to_path_buf());158	let mut files = Vec::new();159	for p in paths {160		let canon = p.canonicalize().unwrap_or_else(|_| p.clone());161		collect_files(&canon, &skip, &mut files);162	}163	files.sort();164	files.dedup();165166	let mut stats = Stats {167		files_scanned: files.len(),168		files_modified: 0,169		imports_rewritten: 0,170	};171	for file in &files {172		let source = match fs::read_to_string(file) {173			Ok(s) => s,174			Err(e) => {175				error!("read {}: {e}", file.display());176				continue;177			}178		};179		let Some((new_source, count)) = rewrite_source(&source, rules) else {180			debug!("unchanged: {}", file.display());181			continue;182		};183		stats.files_modified += 1;184		stats.imports_rewritten += count;185		if dry_run {186			info!("would rewrite {} import(s) in {}", count, file.display());187		} else {188			match fs::write(file, &new_source) {189				Ok(()) => info!("rewrote {} import(s) in {}", count, file.display()),190				Err(e) => error!("write {}: {e}", file.display()),191			}192		}193	}194	stats195}196197#[cfg(test)]198mod tests {199	use super::*;200201	fn rules() -> Vec<Rule> {202		vec![Rule {203			legacy: "kube-prometheus".into(),204			canonical: "github.com/prometheus-operator/kube-prometheus/jsonnet/kube-prometheus"205				.into(),206		}]207	}208209	#[test]210	fn rewrites_prefix() {211		let src = "import 'kube-prometheus/main.libsonnet'\n";212		let (out, n) = rewrite_source(src, &rules()).expect("rewrite");213		assert_eq!(n, 1);214		assert_eq!(215			out,216			"import 'github.com/prometheus-operator/kube-prometheus/jsonnet/kube-prometheus/main.libsonnet'\n"217		);218	}219220	#[test]221	fn rewrites_exact_match() {222		let src = "import \"kube-prometheus\"\n";223		let (out, _n) = rewrite_source(src, &rules()).expect("rewrite");224		assert!(225			out.contains(226				"'github.com/prometheus-operator/kube-prometheus/jsonnet/kube-prometheus'"227			)228		);229	}230231	#[test]232	fn leaves_unrelated_imports_alone() {233		let src = "import 'something/else.libsonnet'\n";234		assert!(rewrite_source(src, &rules()).is_none());235	}236237	#[test]238	fn does_not_match_substring() {239		let src = "import 'kube-prometheus-extra/foo.libsonnet'\n";240		assert!(rewrite_source(src, &rules()).is_none());241	}242}