Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce npv_169 to detect top level withs #142

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 24 additions & 23 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,31 +47,32 @@ The most important tools and commands in this environment are:
### Integration tests

Integration tests are declared in [`./tests`](./tests) as subdirectories imitating Nixpkgs with these files:
- `default.nix`:
Always contains
```nix
import <test-nixpkgs> { root = ./.; }
```
which makes
```
nix-instantiate <subdir> --eval -A <attr> --arg overlays <overlays>
```
work very similarly to the real Nixpkgs, just enough for the program to be able to test it.
- `pkgs/by-name`:
The `pkgs/by-name` directory to check.

- `pkgs/top-level/all-packages.nix` (optional):
Contains an overlay of the form
```nix
self: super: {
# ...
}
```
allowing the simulation of package overrides to the real [`pkgs/top-level/all-packages.nix`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/top-level/all-packages.nix).
The default is an empty overlay.
- `main`: A Nixpkgs root directory with:
- `default.nix`:
Always contains
```nix
import <test-nixpkgs> { root = ./.; }
```
which makes
```
nix-instantiate <subdir> --eval -A <attr> --arg overlays <overlays>
```
work very similarly to the real Nixpkgs, just enough for the program to be able to test it.
- `pkgs/by-name`:
The `pkgs/by-name` directory to check.

- `pkgs/top-level/all-packages.nix` (optional):
Contains an overlay of the form
```nix
self: super: {
# ...
}
```
allowing the simulation of package overrides to the real [`pkgs/top-level/all-packages.nix`](https://github.com/NixOS/nixpkgs/blob/master/pkgs/top-level/all-packages.nix).
The default is an empty overlay.

- `base` (optional):
Contains another subdirectory imitating Nixpkgs with potentially any of the above structures.
Contains another Nixpkgs root directory with potentially any of the above structures.
This is used to test [ratchet checks](./README.md#ratchet-checks).

- `expected` (optional):
Expand Down
9 changes: 4 additions & 5 deletions src/eval.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::{env, fs, process};

Expand Down Expand Up @@ -156,7 +157,7 @@ pub fn check_values(
nixpkgs_path: &Path,
nix_file_store: &mut NixFileStore,
package_names: &[String],
) -> validation::Result<ratchet::Nixpkgs> {
) -> validation::Result<BTreeMap<String, ratchet::Package>> {
let work_dir = tempfile::Builder::new()
.prefix("nixpkgs-vet")
.tempdir()
Expand Down Expand Up @@ -255,9 +256,7 @@ pub fn check_values(
.collect_vec()?,
);

Ok(check_result.map(|elems| ratchet::Nixpkgs {
packages: elems.into_iter().collect(),
}))
Ok(check_result.map(|elems| elems.into_iter().collect()))
}

/// Handle the evaluation result for an attribute in `pkgs/by-name`, making it a validation result.
Expand Down Expand Up @@ -372,7 +371,7 @@ fn by_name(

// Independently report problems about whether it's a derivation and the callPackage
// variant.
is_derivation_result.and(variant_result)
is_derivation_result.and_(variant_result)
}
};
Ok(
Expand Down
116 changes: 116 additions & 0 deletions src/files.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use crate::problem::npv_169;
use crate::ratchet::RatchetState;
use relative_path::RelativePath;
use relative_path::RelativePathBuf;

use rnix::SyntaxKind;
use rowan::ast::AstNode;
use std::collections::BTreeMap;
use std::path::Path;

use crate::nix_file::NixFileStore;
use crate::validation::ResultIteratorExt;
use crate::validation::Validation::Success;
use crate::{nix_file, ratchet, structure, validation};

fn find_invalid_withs(syntax: &rnix::SyntaxNode) -> Option<rnix::SyntaxNode> {
syntax
.descendants()
.filter(|node| node.kind() == rnix::SyntaxKind::NODE_WITH)
.filter(|node| {
node.descendants()
.map(|child| {
if child == *node {
return None;
}
match child.kind() {
SyntaxKind::NODE_WITH => Some(node),
SyntaxKind::NODE_LET_IN => Some(node),
SyntaxKind::NODE_ATTR_SET => Some(node),
_ => None,
}
})
.any(|node| node.is_some())
})
.take(1)
.last()
}

pub fn check_files(
nixpkgs_path: &Path,
nix_file_store: &mut NixFileStore,
) -> validation::Result<BTreeMap<RelativePathBuf, ratchet::File>> {
process_nix_files(nixpkgs_path, nix_file_store, |nix_file| {
Ok(Success(ratchet::File {
top_level_with: check_files_top_level_with_lib(nixpkgs_path, nix_file),
}))
})
}

fn check_files_top_level_with_lib(
nixpkgs_path: &Path,
nix_file: &nix_file::NixFile,
) -> RatchetState<ratchet::DoesNotIntroduceToplevelWiths> {
if let Some(open_scope_with_lib) = find_invalid_withs(nix_file.syntax_root.syntax()) {
RatchetState::Loose(
{
npv_169::TopLevelWithMayShadowVariablesAndBreakStaticChecks::new(
RelativePathBuf::from_path(
nix_file.path.clone().strip_prefix(nixpkgs_path).unwrap(),
)
.unwrap(),
open_scope_with_lib.to_string(),
)
}
.into(),
)
} else {
RatchetState::Tight
}
}

fn collect_nix_files(
base: &Path,
dir: &RelativePath,
files: &mut Vec<RelativePathBuf>,
) -> anyhow::Result<()> {
for entry in structure::read_dir_sorted(&dir.to_path(base))? {
let mut relative_path = dir.to_relative_path_buf();
relative_path.push(entry.file_name().to_string_lossy().into_owned());

let absolute_path = entry.path();

if absolute_path.is_symlink() {
continue;
}
if absolute_path.is_dir() {
collect_nix_files(base, &relative_path, files)?
} else if absolute_path.extension().is_some_and(|x| x == "nix") {
files.push(relative_path)
}
}
Ok(())
}

fn process_nix_files<F: Fn(&nix_file::NixFile) -> validation::Result<ratchet::File>>(
nixpkgs_path: &Path,
nix_file_store: &mut NixFileStore,
f: F,
) -> validation::Result<BTreeMap<RelativePathBuf, ratchet::File>> {
let files = {
let mut files = vec![];
collect_nix_files(nixpkgs_path, &RelativePathBuf::new(), &mut files)?;
files
};

let file_results: Vec<validation::Validation<(RelativePathBuf, ratchet::File)>> = files
.into_iter()
.map(|path| {
let nix_file = nix_file_store.get(&path.to_path(nixpkgs_path))?;
let val = f(nix_file)?.map(|file| (path, file));
Ok::<_, anyhow::Error>(val)
})
.collect_vec()?;

Ok(validation::sequence(file_results).map(|entries| entries.into_iter().collect()))
}
40 changes: 26 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
// #![allow(clippy::missing_const_for_fn)]

mod eval;
mod files;
mod location;
mod nix_file;
mod problem;
Expand All @@ -21,6 +22,7 @@ mod validation;

use anyhow::Context as _;
use clap::Parser;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::{panic, thread};
Expand Down Expand Up @@ -113,20 +115,30 @@ fn check_nixpkgs(nixpkgs_path: &Path) -> validation::Result<ratchet::Nixpkgs> {
)
})?;

if !nixpkgs_path.join(structure::BASE_SUBPATH).exists() {
// No pkgs/by-name directory, always valid
return Ok(Success(ratchet::Nixpkgs::default()));
}

let mut nix_file_store = NixFileStore::default();
let structure = check_structure(&nixpkgs_path, &mut nix_file_store)?;

// Only if we could successfully parse the structure, we do the evaluation checks
let result = structure.result_map(|package_names| {
eval::check_values(&nixpkgs_path, &mut nix_file_store, package_names.as_slice())
})?;
let package_result = {
if !nixpkgs_path.join(structure::BASE_SUBPATH).exists() {
// No pkgs/by-name directory, always valid
Success(BTreeMap::new())
} else {
let structure = check_structure(&nixpkgs_path, &mut nix_file_store)?;

// Only if we could successfully parse the structure, we do the evaluation checks
structure.result_map(|package_names| {
eval::check_values(&nixpkgs_path, &mut nix_file_store, package_names.as_slice())
})?
}
};

let file_result = files::check_files(&nixpkgs_path, &mut nix_file_store)?;

Ok(result)
Ok(
package_result.and(file_result, |packages, files| ratchet::Nixpkgs {
packages,
files,
}),
)
}

#[cfg(test)]
Expand Down Expand Up @@ -178,7 +190,7 @@ mod tests {
return Ok(());
}

let base = path.join(BASE_SUBPATH);
let base = path.join("main").join(BASE_SUBPATH);

fs::create_dir_all(base.join("fo/foo"))?;
fs::write(base.join("fo/foo/package.nix"), "{ someDrv }: someDrv")?;
Expand Down Expand Up @@ -237,7 +249,7 @@ mod tests {
.build()
.expect("valid regex");

let path = path.to_owned();
let main_path = path.join("main");
let base_path = path.join("base");
let base_nixpkgs = if base_path.exists() {
base_path
Expand All @@ -251,7 +263,7 @@ mod tests {
let nix_conf_dir = nix_conf_dir.path().as_os_str();

let status = temp_env::with_var("NIX_CONF_DIR", Some(nix_conf_dir), || {
process(base_nixpkgs, &path)
process(base_nixpkgs, &main_path)
});

let actual_errors = format!("{status}\n");
Expand Down
7 changes: 7 additions & 0 deletions src/problem/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ pub mod npv_161;
pub mod npv_162;
pub mod npv_163;

pub mod npv_169;

#[derive(Clone, Display, EnumFrom)]
pub enum Problem {
/// NPV-100: attribute is not defined but it should be defined automatically
Expand Down Expand Up @@ -123,6 +125,11 @@ pub enum Problem {
NewTopLevelPackageShouldBeByNameWithCustomArgument(
npv_163::NewTopLevelPackageShouldBeByNameWithCustomArgument,
),

/// NPV-169: top-level with may shadow variables and break static checks
TopLevelWithMayShadowVariablesAndBreakStaticChecks(
npv_169::TopLevelWithMayShadowVariablesAndBreakStaticChecks,
),
}

fn indent_definition(column: usize, definition: &str) -> String {
Expand Down
21 changes: 21 additions & 0 deletions src/problem/npv_169.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use relative_path::RelativePathBuf;
use std::fmt;

#[derive(Clone)]
pub struct TopLevelWithMayShadowVariablesAndBreakStaticChecks {
file: RelativePathBuf,
node: String,
}

impl TopLevelWithMayShadowVariablesAndBreakStaticChecks {
pub fn new(file: RelativePathBuf, node: String) -> Self {
Self { file, node }
}
}

impl fmt::Display for TopLevelWithMayShadowVariablesAndBreakStaticChecks {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let Self { file, node: _node } = self;
write!(f, "- {file}: Top level with is discouraged as it may shadow variables and break static checks.")
}
}
33 changes: 33 additions & 0 deletions src/ratchet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//!
//! Each type has a `compare` method that validates the ratchet checks for that item.

use relative_path::RelativePath;
use std::collections::BTreeMap;

use relative_path::RelativePathBuf;
Expand All @@ -15,6 +16,7 @@ use crate::validation::{self, Validation, Validation::Success};
pub struct Nixpkgs {
/// The ratchet values for all packages
pub packages: BTreeMap<String, Package>,
pub files: BTreeMap<RelativePathBuf, File>,
}

impl Nixpkgs {
Expand All @@ -27,6 +29,9 @@ impl Nixpkgs {
.into_iter()
.map(|(name, pkg)| Package::compare(&name, from.packages.get(&name), &pkg)),
)
.and_(validation::sequence_(to.files.into_iter().map(
|(name, file)| File::compare(&name, from.files.get(&name), &file),
)))
}
}

Expand Down Expand Up @@ -57,6 +62,21 @@ impl Package {
}
}

pub struct File {
pub top_level_with: RatchetState<DoesNotIntroduceToplevelWiths>,
}

impl File {
/// Validates the ratchet checks for a top-level package
pub fn compare(name: &RelativePath, optional_from: Option<&Self>, to: &Self) -> Validation<()> {
validation::sequence_([RatchetState::compare(
name.as_str(),
optional_from.map(|x| &x.top_level_with),
&to.top_level_with,
)])
}
}

/// The ratchet state of a generic ratchet check.
pub enum RatchetState<Ratchet: ToProblem> {
/// The ratchet is loose. It can be tightened more. In other words, this is the legacy state
Expand Down Expand Up @@ -173,3 +193,16 @@ impl ToProblem for UsesByName {
}
}
}

// The ratchet value of an attribute for the check that new nixpkgs changes do not
// introduce top level with or withs that could shadow scope.

pub enum DoesNotIntroduceToplevelWiths {}

impl ToProblem for DoesNotIntroduceToplevelWiths {
type ToContext = Problem;

fn to_problem(_name: &str, _optional_from: Option<()>, to: &Self::ToContext) -> Problem {
to.clone()
}
}
Loading
Loading