use std::{path::PathBuf, fs, process::exit}; use rowan::ast::AstNode; use clap::{arg, Parser}; use queries::Query; use changes::{apply_changes, format_range}; use crate::changes::format_range_json; #[allow(dead_code)] mod queries; #[allow(dead_code)] mod status_reporter; mod batchmode; #[allow(dead_code)] mod util; #[allow(dead_code)] mod pipeline; mod changes; #[derive(clap::Parser)] struct Args { query: String, path: Vec, #[arg(short, long)] debug: bool, #[arg(long)] batchmode: bool, #[arg(long)] print_positions: bool, #[arg(long)] json: bool } fn main() { let args = Args::parse(); let query = parse_query(&args.query); if args.debug { println!("{query:?}"); } if args.batchmode { macro_rules! warn_arg { ($arg: ident, $text:literal) => { if args.$arg { eprintln!("Warning: option {} is ignored in batch mode.", $text); }} } warn_arg!(json, "--json"); warn_arg!(print_positions, "--print-positions"); batchmode::batchmode(args.path, query, args.debug); } else { for path in &args.path { handle_file(path, &args, &query) } } } fn handle_file(path: &PathBuf, args: &Args, query: &Query) { let (content, nexp) = match parse_nixfile(path) { Err(e) => { eprintln!("could not parse file: {e}"); exit(2); }, Ok(exp) => exp }; let (changes, results) = query.apply(&content, nexp.syntax().clone()).unwrap(); if args.debug { println!("{changes:?}"); } if changes.len() == 0 { if args.print_positions { if args.json { let json = results .iter() .map(|result| format_range_json(path, &content, result.text_range())) .collect::>(); println!("{}", serde_json::to_string(&json).unwrap()); } else { for result in results { println!( "{}", format_range(path, &content, result.text_range()) ); } } } else { for result in results { println!("{result}"); } } } else { let changed = apply_changes(&content, changes, args.debug); println!("{changed}"); } } fn parse_nixfile(path: &PathBuf) -> anyhow::Result<(String, rnix::Root)> { let content = fs::read_to_string(path)?; let tree = parse_nexpr(&content)?; Ok((content, tree)) } fn parse_nexpr(code: &str) -> anyhow::Result { let parse = rnix::Root::parse(&code); if !parse.errors().is_empty() { anyhow::bail!("error: {:?}", parse.errors()); } Ok(parse.tree()) } fn parse_query(querystring: &str) -> Query { let parse = queries::parse(querystring); if parse.errors.len() != 0 { eprintln!( "syntax {}: \n {}", if parse.errors.len() == 1 { "error" } else { "errors" }, parse.errors.join(" \n") ); exit(1); } let query = match parse.to_query() { Err(es) => { queries::print_query_errors(querystring, es); exit(1); }, Ok(query) => query }; query } macro_rules! test_change { ($name:ident, $query:literal, $before:literal, $after:literal) => { #[test] fn $name() { let nix = $before; let nexpr = parse_nexpr(&nix).unwrap(); let query = parse_query($query); let (changes, _) = query.apply(&nix, nexpr.syntax().clone()).unwrap(); let changed = apply_changes(&nix, changes, false); assert_eq!(changed, $after); } } } test_change!( test_remove_inherit, ">> Inherit >> mdDoc[remove]", r#" {lib, ...}: mkDerivation { pname = "dings"; inherit (lib) mdDoc a b c mdDoc; }"#, r#" {lib, ...}: mkDerivation { pname = "dings"; inherit (lib) a b c; }"# ); test_change!( test_remove_attrpath, ">> mkDerivation >> pname[remove]", r#" {lib, ...}: mkDerivation { pname = "dings"; meta.mainProgram = "blub"; }"#, r#" {lib, ...}: mkDerivation { meta.mainProgram = "blub"; }"# );