diff options
Diffstat (limited to '')
-rw-r--r-- | src/main.rs | 408 |
1 files changed, 85 insertions, 323 deletions
diff --git a/src/main.rs b/src/main.rs index 5e86c89..a09f4ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,356 +1,118 @@ -use std::{path::{Path, PathBuf}, fs, str::FromStr, sync::{Arc, Mutex}}; -use anyhow::{Result, Context, anyhow}; -use itertools::Itertools; -use rnix::{SyntaxKind, ast::{AttrpathValue, AttrSet, HasEntry, Entry::*}, SyntaxNode}; +use std::{path::PathBuf, fs, process::exit}; use rowan::{ast::AstNode, TextSize}; -use serde::Deserialize; -use threadpool::ThreadPool; +use clap::{arg, command, value_parser}; -struct StatusReportData { - files: usize, - items: usize, - total_files: usize, - total_items: usize, - changed_items: usize, - last_file: String, - last_item: String, -} - -impl StatusReportData { - fn print(&self, clear: bool) { - if clear { - print!("\x1b[1F\x1b[2K\x1b[1F\x1b[2K"); - } - println!("{}/{} files ({})", self.files, self.total_files, self.last_file); - println!("{}/{} ({}) items ({})", self.items, self.total_items, - self.changed_items, self.last_item); - } -} - -struct StatusReport(Mutex<StatusReportData>); - -impl StatusReport { - fn new(total_files: usize, total_items: usize) -> Self { - Self(Mutex::new(StatusReportData { - files: 0, - items: 0, - total_files, - total_items, - changed_items: 0, - last_file: "".to_string(), - last_item: "".to_string(), - })) - } - - fn enter_file(&self, f: &str) { - let mut m = self.0.lock().unwrap(); - m.files += 1; - m.last_file = f.to_string(); - m.print(m.files > 1 || m.items >= 1); - } - - fn enter_item(&self, i: String) { - let mut m = self.0.lock().unwrap(); - m.items += 1; - m.last_item = i; - m.print(m.files >= 1 || m.items > 1); - } - - fn update_item(&self, i: String) { - let mut m = self.0.lock().unwrap(); - m.last_item = i; - m.print(true); - } - - fn changed_item(&self) { - let mut m = self.0.lock().unwrap(); - m.changed_items += 1; - m.print(true); - } - - fn skip_items(&self, i: usize) { - let mut m = self.0.lock().unwrap(); - m.items += i; - m.print(m.files >= 1 || m.items >= 1); - } -} - -struct StatusPart<'a>(&'a StatusReport, usize); - -impl<'a> StatusPart<'a> { - fn enter_item(&mut self, i: String) { - self.0.enter_item(i); - self.1 -= 1; - } - - fn update_item(&mut self, i: String) { - self.0.update_item(i); - } - - fn changed_item(&mut self) { - self.0.changed_item(); - } -} - -impl<'a> Drop for StatusPart<'a> { - fn drop(&mut self) { - self.0.skip_items(self.1); - } -} - - - -fn textsize_at_line(s: &str, line: usize) -> TextSize { - s - .split('\n') - .map(|l| - TextSize::of(l) + TextSize::new(1) - ) - .take(line-1) - .sum() -} - - -fn dig_to_kind(kind: SyntaxKind, node: &SyntaxNode) -> Option<SyntaxNode> { - if node.kind() == kind { - return Some(node.clone()); - } - - node.descendants() - .filter(|node| node.kind() == kind) - .next() -} - -fn add_to_meta_block(rough_pos: SyntaxNode, content: &str, main_program: &str, file: &Path, line: usize) -> Result<(String, usize)> { - let meta_node = dig_to_kind(SyntaxKind::NODE_ATTR_SET, &rough_pos).unwrap(); - let meta_set = AttrSet::cast(meta_node.clone()).unwrap(); +#[allow(dead_code)] +mod queries; +#[allow(dead_code)] +mod status_reporter; +mod batchmode; +#[allow(dead_code)] +mod util; - let description_entry = meta_set.entries() - .filter(|entry| { - match &entry { - Inherit(it) => it.attrs().any(|c| c.to_string() == "description"), - AttrpathValue(it) => it.attrpath().unwrap().to_string() == "description", - } - }) - .exactly_one().ok() - .with_context(|| format!("meta node has no description attribute in {:?} at {}", file, line))?; - let description = description_entry.syntax(); - let pos = description.text_range(); - let indent = content[..pos.start().into()].chars().rev().position(|c| c == '\n').unwrap(); +fn parse_nexp(path: &PathBuf) -> anyhow::Result<(String, rnix::Root)> { + let content = fs::read_to_string(path)?; - let patch = String::new() - + "\n" - + &" ".repeat(indent) - + "mainProgram = \"" + main_program + "\";"; - - Ok((patch, pos.end().into())) -} - -fn edit_one(file: &Path, line: usize, main_program: &str, p: &StatusReport) -> Result<String> { - let mut content = fs::read_to_string(file)?; - let searchpos = textsize_at_line(&content, line); - - p.update_item(format!("doing {:?}", file)); let parse = rnix::Root::parse(&content); if !parse.errors().is_empty() { anyhow::bail!("error: {:?}", parse.errors()); } - let tree = parse.tree(); - - let pos_node = tree.syntax().descendants() - .filter(|node| { - if node.kind() == SyntaxKind::NODE_ATTRPATH_VALUE { - let value = AttrpathValue::cast(node.clone()).unwrap(); - node.text_range().contains(searchpos) && value.attrpath().unwrap().to_string() == "meta" - } else { false } - }) - .exactly_one().ok(); - - // do we have a meta attrset already? - let (patch, insert_offset) = match pos_node { - None => { - let version_node = tree - .syntax() - .descendants() - .filter(|node| { - if node.kind() == SyntaxKind::NODE_ATTRPATH_VALUE { - let value = AttrpathValue::cast(node.clone()).unwrap(); - let name = value.attrpath().unwrap().to_string(); - node.text_range().contains(searchpos + TextSize::new(5)) - && (name == "version" || name == "pname" || name == "name") - } else { false } - }) - .exactly_one().ok() - .with_context(|| format!("neither meta nor version node found for {:?} at {}", file, line))?; + Ok((content, parse.tree())) +} - let attrset = version_node.parent().unwrap(); - if attrset.kind() != SyntaxKind::NODE_ATTR_SET { - anyhow::bail!("name not in an attrset in {:?} at {}", file, line) - } +fn main() { - // does a meta block already exist? - let maybe_meta_block = attrset - .descendants() - .filter(|node| { - if node.kind() == SyntaxKind::NODE_ATTRPATH_VALUE { - let value = AttrpathValue::cast(node.clone()).unwrap(); - let name = value.attrpath().unwrap().to_string(); - name == "meta" - } else { false } - }) - .exactly_one(); + let matches = command!() + .arg(arg!(--batchmode "run in batch mode") + .required(false) + ) + .arg(arg!([query] "query to run") + .required(true)) + .arg(arg!([file] "file to operate on") + .required(true) + .value_parser(value_parser!(PathBuf)) + ) + .arg(arg!(--edit <operation> "what to do") + .required(false)) + .get_matches(); + + let query_string = matches.get_one::<String>("query").unwrap(); + let files = matches.get_one::<PathBuf>("file").unwrap(); + + let parse = queries::parse(query_string); + if parse.errors.len() != 0 { + eprintln!( + "syntax {}: \n {}", + if parse.errors.len() == 1 { "error" } else { "errors" }, + parse.errors.join(" \n") + ); + exit(1); + } + + let (content, nexp) = match parse_nexp(files) { + Err(e) => { + eprintln!("could not parse file: {e}"); + exit(2); + }, + Ok(exp) => exp + }; - if let Ok(meta) = maybe_meta_block { - add_to_meta_block(meta.clone(), &content, main_program, file, line)? - } else { - let before_attrset_end = Into::<usize>::into(attrset.text_range().end()) - - 1 - - content[..attrset.text_range().end().into()] - .chars().rev().position(|c| c == '\n').unwrap(); + // println!("{nexp:#?}"); - let indent = content[..version_node.text_range().start().into()] - .chars().rev().position(|c| c == '\n').unwrap(); + let results = parse.apply(&content, nexp.syntax().clone()).unwrap(); - // some language specific build systems don't use meta as its own attrset - // there's no good way to recognise these, but this seems to work fine - let weird_nonstandard_meta = attrset - .descendants() - .any(|node| { - if node.kind() == SyntaxKind::NODE_ATTRPATH_VALUE { - let value = AttrpathValue::cast(node.clone()).unwrap(); - let name = value.attrpath().unwrap().to_string(); - name == "description" || name == "homepage" || name == "license" - } else { false } - }); - let patch = String::new() - + "\n" - + &" ".repeat(indent) - + if weird_nonstandard_meta { "mainProgram = \"" } else { "meta.mainProgram = \"" } - + main_program + "\";"; - (patch, before_attrset_end) + if let Some(op) = matches.get_one::<String>("edit") { + match &op[..] { + "remove" => { + let new = remove_nodes(content, &results); + println!("{new}"); } - }, - Some(pos) => { - add_to_meta_block(pos.clone(), &content, main_program, file, line)? + _ => () } - }; - - - content = String::new() - + &content[..insert_offset] - + &patch - + &content[insert_offset..]; - - p.changed_item(); - Ok(content) -} + } else { + for result in &results { + println!("{result}"); + } + } -#[derive(Deserialize, Clone)] -struct TrivialProgram { - pos: Option<String>, - name: String } -fn main() { - - let raw_inputs = fs::read_to_string("trivials.json").unwrap(); - let inputs: Vec<TrivialProgram> = serde_json::de::from_str::<Vec<_>>(&raw_inputs).unwrap(); - // .into_iter().take(200).collect(); +fn remove_nodes(content: String, results: &Vec<rnix::SyntaxNode>) -> String { + assert!(results.len() == 1); - // TODO: group edits in the same file - let pool = ThreadPool::new(16); + let span = &results[0]; - let mut tasks: Vec<(TrivialProgram, PathBuf, usize)> = inputs.into_iter() - .filter_map(|i| { - if i.pos.is_none() { - println!("no position for name {}", i.name); - None + let (before, after) = match (span.prev_sibling_or_token(), span.next_sibling_or_token()) { + (Some(prev), Some(next)) + if prev.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE + && next.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE + => { + if prev.to_string().lines().count() < next.to_string().lines().count() { + (prev.text_range().len(), TextSize::new(0)) } else { - let pos = i.pos.as_ref().unwrap(); - let (filename, line) = { - let l = pos.split(':').collect::<Vec<_>>(); - assert!(l.len() == 2); - - (PathBuf::from_str(l[0]).unwrap(), l[1].parse().unwrap()) - }; - Some((i, filename, line)) - } - }) - .collect(); - - tasks.sort_by_key(|(_ ,filename, _)| filename.clone()); - - let grouped_tasks: Vec<(TrivialProgram, PathBuf, Vec<(usize, String)>)> = - tasks.into_iter() - .map(|(i, f, l)| (i.clone(), f, vec![(l, i.name)])) - .coalesce(|(i1, f1, l1), (i2, f2, l2)| { - if f1 == f2 { - if l1 == l2 && i1.name == i2.name { - Ok((i1, f1, l1)) - } else { - Ok((i1, f1, l1.into_iter().chain(l2.into_iter()).collect())) - } - } else { - Err(((i1,f1,l1),(i2,f2,l2))) - } - }).collect(); - - let results = Arc::new(Mutex::new(vec![])); - let printer = Arc::new(StatusReport::new(grouped_tasks.len(), grouped_tasks.len())); - - for (i, filename, sites) in grouped_tasks { - pool.execute({ - let results = Arc::clone(&results); - let printer = Arc::clone(&printer); - - move || { - let pos = i.pos.unwrap(); - printer.enter_file(&pos); - if sites.len() == 1 { - let result = edit_one(&filename, sites[0].0, &sites[0].1, &printer) - .map(|ok| (filename, ok)); - results.lock().unwrap().push(result); - } else { - results.lock().unwrap().push(Err(anyhow!("skipped {:?} as it has multiple edits", filename))); - } + (TextSize::new(0), next.text_range().len()) } - }); - } - - pool.join(); - - println!("\n\nSummary:"); - let mut c_errors = 0; - let mut c_total = 0; - for r in results.lock().unwrap().iter() { - match r { - Err(e) => { - println!(" {}", e); - c_errors += 1; - }, - _ => () } - c_total += 1; - } + _ => (TextSize::default(),TextSize::default()) + }; + + String::new() + + &content[..Into::<usize>::into(span.text_range().start() - before) - previous_indentation(span).unwrap_or(0)] + + &content[(span.text_range().end() + after).into()..] +} - println!("\n ({c_total} sites total, {c_errors} errors, generated {} edits)", c_total - c_errors); - let edits: Vec<_> = Arc::into_inner(results).unwrap().into_inner().unwrap() - .into_iter() - .filter_map(|r| r.ok()) - .collect(); - // check we didn't miss any duplicate edits - let duplicates = edits.iter().duplicates_by(|(filename, _)| filename).count(); - println!("{duplicates} edits were not the only one in their file"); +fn previous_indentation(node: &rnix::SyntaxNode) -> Option<usize> { + let whitespace_token = node.prev_sibling_or_token()?; - println!("applying changes ..."); - for (filename, content) in edits { - fs::write(&filename, content.as_bytes()).unwrap(); - // println!("{}", content); + if whitespace_token.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE { + Some(whitespace_token.to_string().lines().last().unwrap().len()) + } else { + None } } |