Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
infinisil committed Feb 19, 2024
1 parent 03bd6ed commit 3805b88
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 39 deletions.
5 changes: 5 additions & 0 deletions notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- SyntaxNodes are either mutable or not
- We want them to be mutable, but only when we actually intend on changing them
This means, only for the `migrate` command, and only for the attr name location files, not the reference check files
- We should be able to re-render the file without needing backward-replacements
- Mutable syntax nodes can be changed without absolute locations/indices. So any absolute locations need to be resolved before modifying it
28 changes: 21 additions & 7 deletions pkgs/test/nixpkgs-check-by-name/src/eval.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::nixpkgs_problem::NixpkgsProblem;
pub(crate) use crate::nixpkgs_problem::NixpkgsProblem;
use crate::ratchet;
use crate::structure;
use crate::utils;
Expand Down Expand Up @@ -405,14 +405,18 @@ fn handle_non_by_name_attribute(
location: Some(location),
}) = non_by_name_attribute {

let nix_file = nix_file_store.get(&location.file)?;

// Parse the Nix file in the location and figure out whether it's an
// attribute definition of the form `= callPackage <arg1> <arg2>`,
// returning the arguments if so.
let optional_syntactic_call_package = nix_file_store
.get(&location.file)?
.call_package_argument_info_at(
location.line,
location.column,
if let Some(attrpath_value_node) = nix_file.attrpath_value_at(location.line, location.column)? {

// Parse the Nix file in the location and figure out whether it's an
// attribute definition of the form `= callPackage <arg1> <arg2>`,
// returning the arguments if so.
let optional_syntactic_call_package = nix_file.attrpath_value_call_package_argument_info(
&attrpath_value_node,
// Passing the Nixpkgs path here both checks that the <arg1> is within Nixpkgs, and
// strips the absolute Nixpkgs path from it, such that
// syntactic_call_package.relative_path is relative to Nixpkgs
Expand Down Expand Up @@ -453,11 +457,21 @@ fn handle_non_by_name_attribute(
_ => {
// Otherwise, the path is outside `pkgs/by-name`, which means it can be
// migrated
Loose(syntactic_call_package)
Loose(ratchet::UsesByNameContext {
call_package_argument_info: syntactic_call_package,
file: location.file.to_owned(),
line: location.line,
syntax_node: attrpath_value_node,
})
}
}
}
}
} else {
// Inherit
NonApplicable
}

} else {
// This catches all the cases not matched by the above `if let`, falling back to not being
// able to migrate such attributes
Expand Down
72 changes: 57 additions & 15 deletions pkgs/test/nixpkgs-check-by-name/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::io::Write;
use crate::nix_file::NixFileStore;
mod eval;
mod nix_file;
Expand All @@ -13,13 +14,24 @@ use crate::validation::Validation::Failure;
use crate::validation::Validation::Success;
use anyhow::Context;
use clap::Parser;
use clap::Subcommand;
use colored::Colorize;
use std::io;
use std::path::{Path, PathBuf};
use std::process::ExitCode;

/// Program to check the validity of pkgs/by-name
///
#[derive(Parser, Debug)]
#[command(about, verbatim_doc_comment)]
pub struct Args {
#[command(subcommand)]
command: Commands,

/// Path to the main Nixpkgs to check.
/// For PRs, this should be set to a checkout of the PR branch.
nixpkgs: PathBuf,
}

/// This CLI interface may be changed over time if the CI workflow making use of
/// it is adjusted to deal with the change appropriately.
///
Expand All @@ -31,22 +43,26 @@ use std::process::ExitCode;
/// Standard error:
/// - Informative messages
/// - Detected problems if validation is not successful
#[derive(Parser, Debug)]
#[command(about, verbatim_doc_comment)]
pub struct Args {
/// Path to the main Nixpkgs to check.
/// For PRs, this should be set to a checkout of the PR branch.
nixpkgs: PathBuf,

#[derive(Subcommand, Debug)]
enum Commands {
/// Checks whether PR is valid
CheckPR {
/// Path to the base Nixpkgs to run ratchet checks against.
/// For PRs, this should be set to a checkout of the PRs base branch.
#[arg(long)]
base: PathBuf,
},

/// Migrates a Nixpkgs
Migrate,
}

fn main() -> ExitCode {
let args = Args::parse();
match process(&args.base, &args.nixpkgs, false, &mut io::stderr()) {
let mut nix_file_store = NixFileStore::default();
match args.command {
Commands::CheckPR { base } => {
match process(&base, &args.nixpkgs, &mut nix_file_store, false, &mut io::stderr()) {
Ok(true) => {
eprintln!("{}", "Validated successfully".green());
ExitCode::SUCCESS
Expand All @@ -61,6 +77,29 @@ fn main() -> ExitCode {
}
}
}
Commands::Migrate => {
let mut error_writer = &mut io::stderr();
match check_nixpkgs(&args.nixpkgs, &mut nix_file_store, false, &mut error_writer) {
Ok(validation::Validation::Success(nixpkgs)) => {
nixpkgs.migrate(&mut nix_file_store);
eprintln!("{}", "Migration done".green());
ExitCode::SUCCESS
}
Ok(validation::Validation::Failure(errors)) => {
for error in errors {
writeln!(error_writer, "{}", error.to_string().red()).unwrap()
}
eprintln!("{}", "Validation failed, see above errors".yellow());
ExitCode::from(1)
}
Err(e) => {
eprintln!("{} {:#}", "I/O error: ".yellow(), e);
ExitCode::from(2)
}
}
}
}
}

/// Does the actual work. This is the abstraction used both by `main` and the tests.
///
Expand All @@ -79,15 +118,16 @@ fn main() -> ExitCode {
pub fn process<W: io::Write>(
base_nixpkgs: &Path,
main_nixpkgs: &Path,
nix_file_store: &mut NixFileStore,
keep_nix_path: bool,
error_writer: &mut W,
) -> anyhow::Result<bool> {
// Check the main Nixpkgs first
let main_result = check_nixpkgs(main_nixpkgs, keep_nix_path, error_writer)?;
let main_result = check_nixpkgs(main_nixpkgs, nix_file_store, keep_nix_path, error_writer)?;
let check_result = main_result.result_map(|nixpkgs_version| {
// If the main Nixpkgs doesn't have any problems, run the ratchet checks against the base
// Nixpkgs
check_nixpkgs(base_nixpkgs, keep_nix_path, error_writer)?.result_map(
check_nixpkgs(base_nixpkgs, nix_file_store, keep_nix_path, error_writer)?.result_map(
|base_nixpkgs_version| {
Ok(ratchet::Nixpkgs::compare(
base_nixpkgs_version,
Expand Down Expand Up @@ -115,10 +155,10 @@ pub fn process<W: io::Write>(
/// ratchet check against another result.
pub fn check_nixpkgs<W: io::Write>(
nixpkgs_path: &Path,
nix_file_store: &mut NixFileStore,
keep_nix_path: bool,
error_writer: &mut W,
) -> validation::Result<ratchet::Nixpkgs> {
let mut nix_file_store = NixFileStore::default();

Ok({
let nixpkgs_path = nixpkgs_path.canonicalize().with_context(|| {
Expand All @@ -136,15 +176,16 @@ pub fn check_nixpkgs<W: io::Write>(
)?;
Success(ratchet::Nixpkgs::default())
} else {
check_structure(&nixpkgs_path, &mut nix_file_store)?.result_map(|package_names|
check_structure(&nixpkgs_path, nix_file_store)?.result_map(|package_names|
// Only if we could successfully parse the structure, we do the evaluation checks
eval::check_values(&nixpkgs_path, &mut nix_file_store, package_names, keep_nix_path))?
eval::check_values(&nixpkgs_path, nix_file_store, package_names, keep_nix_path))?
}
})
}

#[cfg(test)]
mod tests {
use crate::NixFileStore;
use crate::process;
use crate::utils;
use anyhow::Context;
Expand Down Expand Up @@ -240,7 +281,8 @@ mod tests {
// We don't want coloring to mess up the tests
let writer = temp_env::with_var("NO_COLOR", Some("1"), || -> anyhow::Result<_> {
let mut writer = vec![];
process(base_nixpkgs, &path, true, &mut writer)
let mut nix_file_store = NixFileStore::default();
process(base_nixpkgs, &path, &mut nix_file_store, true, &mut writer)
.with_context(|| format!("Failed test case {name}"))?;
Ok(writer)
})?;
Expand Down
97 changes: 86 additions & 11 deletions pkgs/test/nixpkgs-check-by-name/src/nix_file.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! This is a utility module for interacting with the syntax of Nix files

use std::ops::Range;
use std::io::Write;
use std::fs::File;
use crate::utils::LineIndex;
use anyhow::Context;
use rnix::ast;
Expand Down Expand Up @@ -34,6 +37,15 @@ impl NixFileStore {
Entry::Vacant(entry) => Ok(entry.insert(NixFile::new(path)?)),
}
}

//pub fn render(&mut self) -> anyhow::Result<()> {
// for (_path, file) in &mut self.entries {
// if file.needs_rendering {
// file.render()?
// }
// }
// Ok(())
//}
}

/// A structure for storing a successfully parsed Nix file
Expand All @@ -42,8 +54,66 @@ pub struct NixFile {
pub parent_dir: PathBuf,
/// The path to the file itself, for errors
pub path: PathBuf,
pub syntax_root: rnix::Root,
pub root_node: rnix::SyntaxNode,
pub line_index: LineIndex,


/// The queued updates to it
pub mutable_root_node: Option<rnix::SyntaxNode>,
}

pub struct Chunk<'a> {
contents: &'a [u8],
}

pub struct MutableFile<'a> {
chunks: Vec<&'a [u8]>,
}

impl<'a> MutableFile<'a> {

}

pub struct Edit {
remove: Range<usize>,
insert: Vec<u8>,
}


// Goal: Don't return any SyntaxNode's?

impl NixFile {

pub fn mutable_root_node(&mut self) -> &rnix::SyntaxNode {
if let Some(ref node) = self.mutable_root_node {
node
} else {
self.mutable_root_node = Some(self.root_node.clone_for_update());
self.mutable_root_node.as_ref().unwrap()
}
}

//pub fn update_root_node(&mut self, green: rowan::GreenNode) {
// self.root_node = rowan::SyntaxNode::new_root(green);
// self.needs_rendering = true;
//}

pub fn render(&mut self) -> anyhow::Result<()> {
if let Some(ref node) = self.mutable_root_node {
let mut file = File::create(&self.path)?;

let bytes = node.to_string().into_bytes();

file.write_all(&bytes)?;

self.mutable_root_node = None;

Ok(())
} else {
Ok(())
}
}

}

impl NixFile {
Expand All @@ -68,8 +138,9 @@ impl NixFile {
.map(|syntax_root| NixFile {
parent_dir: parent_dir.to_path_buf(),
path: path.as_ref().to_owned(),
syntax_root,
root_node: syntax_root.syntax().clone(),
line_index,
mutable_root_node: None,
})
.with_context(|| format!("Could not parse file {} with rnix", path.as_ref().display()))
}
Expand Down Expand Up @@ -123,20 +194,19 @@ impl NixFile {
let Some(attrpath_value) = self.attrpath_value_at(line, column)? else {
return Ok(None);
};
self.attrpath_value_call_package_argument_info(attrpath_value, relative_to)
self.attrpath_value_call_package_argument_info(&attrpath_value, relative_to)
}

// Internal function mainly to make it independently testable
fn attrpath_value_at(
pub fn attrpath_value_at(
&self,
line: usize,
column: usize,
) -> anyhow::Result<Option<ast::AttrpathValue>> {
) -> anyhow::Result<Option<rnix::SyntaxNode>> {
let index = self.line_index.fromlinecolumn(line, column);

let token_at_offset = self
.syntax_root
.syntax()
.root_node
.token_at_offset(TextSize::from(index as u32));

// The token_at_offset function takes indices to mean a location _between_ characters,
Expand Down Expand Up @@ -178,26 +248,31 @@ impl NixFile {
)
};

if !ast::AttrpathValue::can_cast(attrpath_value_node.kind()) {
if attrpath_value_node.kind() != SyntaxKind::NODE_ATTRPATH_VALUE {
anyhow::bail!(
"Node in {} is not an attribute path value node: {attrpath_value_node:?}",
self.path.display()
)
}

// attrpath_value_node looks like "foo.bar = 10;"


// unwrap is fine because we confirmed that we can cast with the above check.
// We could avoid this `unwrap` for a `clone`, since `cast` consumes the argument,
// but we still need it for the error message when the cast fails.
Ok(Some(ast::AttrpathValue::cast(attrpath_value_node).unwrap()))
Ok(Some(attrpath_value_node))
}

// Internal function mainly to make attrpath_value_at independently testable
fn attrpath_value_call_package_argument_info(
pub fn attrpath_value_call_package_argument_info(
&self,
attrpath_value: ast::AttrpathValue,
attrpath_value_node: &rnix::SyntaxNode,
relative_to: &Path,
) -> anyhow::Result<Option<CallPackageArgumentInfo>> {

let attrpath_value = ast::AttrpathValue::cast(attrpath_value_node.to_owned()).unwrap();

let Some(attrpath) = attrpath_value.attrpath() else {
anyhow::bail!("attrpath value node doesn't have an attrpath: {attrpath_value:?}")
};
Expand Down

0 comments on commit 3805b88

Please sign in to comment.