Skip to content

Commit 74de0ce

Browse files
philiptaronlucasew
andcommitted
NPV-169: detect top-level with expressions that shadow scope
Add a ratchet check that prevents introducing new top-level `with` expressions whose body contains attr sets, let-in bindings, or nested with expressions. Existing instances are grandfathered via the ratchet. Co-authored-by: Lucas Eduardo Wendt <lucas59356@gmail.com>
1 parent 3ec2e01 commit 74de0ce

File tree

32 files changed

+477
-42
lines changed

32 files changed

+477
-42
lines changed

src/files.rs

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
use crate::problem::npv_169;
2+
use crate::ratchet::RatchetState;
13
use relative_path::RelativePath;
24
use relative_path::RelativePathBuf;
5+
6+
use rnix::SyntaxKind;
7+
use rowan::ast::AstNode;
38
use std::collections::BTreeMap;
49
use std::path::Path;
510

@@ -8,50 +13,62 @@ use crate::validation::ResultIteratorExt;
813
use crate::validation::Validation::Success;
914
use crate::{nix_file, ratchet, structure, validation};
1015

11-
/// Runs check on all Nix files, returning a ratchet result for each
16+
fn find_invalid_withs(syntax: &rnix::SyntaxNode) -> Option<rnix::SyntaxNode> {
17+
syntax
18+
.descendants()
19+
.filter(|node| node.kind() == rnix::SyntaxKind::NODE_WITH)
20+
.filter(|node| {
21+
node.descendants()
22+
.map(|child| {
23+
if child == *node {
24+
return None;
25+
}
26+
match child.kind() {
27+
SyntaxKind::NODE_WITH => Some(node),
28+
SyntaxKind::NODE_LET_IN => Some(node),
29+
SyntaxKind::NODE_ATTR_SET => Some(node),
30+
_ => None,
31+
}
32+
})
33+
.any(|node| node.is_some())
34+
})
35+
.take(1)
36+
.last()
37+
}
38+
1239
pub fn check_files(
1340
nixpkgs_path: &Path,
1441
nix_file_store: &mut NixFileStore,
1542
) -> validation::Result<BTreeMap<RelativePathBuf, ratchet::File>> {
16-
process_nix_files(nixpkgs_path, nix_file_store, |_nix_file| {
17-
// Noop for now, only boilerplate to make it easier to add future file-based checks
18-
Ok(Success(ratchet::File {}))
43+
process_nix_files(nixpkgs_path, nix_file_store, |nix_file| {
44+
Ok(Success(ratchet::File {
45+
top_level_with: check_files_top_level_with_lib(nixpkgs_path, nix_file),
46+
}))
1947
})
2048
}
2149

22-
/// Processes all Nix files in a Nixpkgs directory according to a given function `f`, collecting the
23-
/// results into a mapping from each file to a ratchet value.
24-
fn process_nix_files(
50+
fn check_files_top_level_with_lib(
2551
nixpkgs_path: &Path,
26-
nix_file_store: &mut NixFileStore,
27-
f: impl Fn(&nix_file::NixFile) -> validation::Result<ratchet::File>,
28-
) -> validation::Result<BTreeMap<RelativePathBuf, ratchet::File>> {
29-
// Get all Nix files
30-
let files = {
31-
let mut files = vec![];
32-
collect_nix_files(nixpkgs_path, &RelativePathBuf::new(), &mut files)?;
33-
files
34-
};
35-
36-
let results = files
37-
.into_iter()
38-
.map(|path| {
39-
// Get the (optionally-cached) parsed Nix file
40-
let nix_file = nix_file_store.get(&path.to_path(nixpkgs_path))?;
41-
let result = f(nix_file)?;
42-
let val = result.map(|ratchet| (path, ratchet));
43-
Ok::<_, anyhow::Error>(val)
44-
})
45-
.collect_vec()?;
46-
47-
Ok(validation::sequence(results).map(|entries| {
48-
// Convert the Vec to a BTreeMap
49-
entries.into_iter().collect()
50-
}))
52+
nix_file: &nix_file::NixFile,
53+
) -> RatchetState<ratchet::DoesNotIntroduceToplevelWiths> {
54+
if let Some(open_scope_with_lib) = find_invalid_withs(nix_file.syntax_root.syntax()) {
55+
RatchetState::Loose(
56+
{
57+
npv_169::TopLevelWithMayShadowVariablesAndBreakStaticChecks::new(
58+
RelativePathBuf::from_path(
59+
nix_file.path.clone().strip_prefix(nixpkgs_path).unwrap(),
60+
)
61+
.unwrap(),
62+
open_scope_with_lib.to_string(),
63+
)
64+
}
65+
.into(),
66+
)
67+
} else {
68+
RatchetState::Tight
69+
}
5170
}
5271

53-
/// Recursively collects all Nix files in the relative `dir` within `base`
54-
/// into the `files` `Vec`.
5572
fn collect_nix_files(
5673
base: &Path,
5774
dir: &RelativePath,
@@ -63,7 +80,6 @@ fn collect_nix_files(
6380

6481
let absolute_path = entry.path();
6582

66-
// We'll get to every file based on directory recursion, no need to follow symlinks.
6783
if absolute_path.is_symlink() {
6884
continue;
6985
}
@@ -75,3 +91,26 @@ fn collect_nix_files(
7591
}
7692
Ok(())
7793
}
94+
95+
fn process_nix_files<F: Fn(&nix_file::NixFile) -> validation::Result<ratchet::File>>(
96+
nixpkgs_path: &Path,
97+
nix_file_store: &mut NixFileStore,
98+
f: F,
99+
) -> validation::Result<BTreeMap<RelativePathBuf, ratchet::File>> {
100+
let files = {
101+
let mut files = vec![];
102+
collect_nix_files(nixpkgs_path, &RelativePathBuf::new(), &mut files)?;
103+
files
104+
};
105+
106+
let file_results: Vec<validation::Validation<(RelativePathBuf, ratchet::File)>> = files
107+
.into_iter()
108+
.map(|path| {
109+
let nix_file = nix_file_store.get(&path.to_path(nixpkgs_path))?;
110+
let val = f(nix_file)?.map(|file| (path, file));
111+
Ok::<_, anyhow::Error>(val)
112+
})
113+
.collect_vec()?;
114+
115+
Ok(validation::sequence(file_results).map(|entries| entries.into_iter().collect()))
116+
}

src/problem/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ pub mod npv_161;
3636
pub mod npv_162;
3737
pub mod npv_163;
3838

39+
pub mod npv_169;
40+
3941
#[derive(Clone, Display, EnumFrom)]
4042
pub enum Problem {
4143
/// NPV-100: attribute is not defined but it should be defined automatically
@@ -131,6 +133,11 @@ pub enum Problem {
131133
NewTopLevelPackageShouldBeByNameWithCustomArgument(
132134
npv_163::NewTopLevelPackageShouldBeByNameWithCustomArgument,
133135
),
136+
137+
/// NPV-169: top-level with may shadow variables and break static checks
138+
TopLevelWithMayShadowVariablesAndBreakStaticChecks(
139+
npv_169::TopLevelWithMayShadowVariablesAndBreakStaticChecks,
140+
),
134141
}
135142

136143
fn indent_definition(column: usize, definition: &str) -> String {

src/problem/npv_169.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use relative_path::RelativePathBuf;
2+
use std::fmt;
3+
4+
#[derive(Clone)]
5+
pub struct TopLevelWithMayShadowVariablesAndBreakStaticChecks {
6+
file: RelativePathBuf,
7+
node: String,
8+
}
9+
10+
impl TopLevelWithMayShadowVariablesAndBreakStaticChecks {
11+
pub fn new(file: RelativePathBuf, node: String) -> Self {
12+
Self { file, node }
13+
}
14+
}
15+
16+
impl fmt::Display for TopLevelWithMayShadowVariablesAndBreakStaticChecks {
17+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
18+
let Self { file, node: _node } = self;
19+
write!(
20+
f,
21+
"- {file}: Top level with is discouraged as it may shadow variables and break static checks."
22+
)
23+
}
24+
}

src/ratchet.rs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,18 @@ impl Package {
6262
}
6363
}
6464

65-
pub struct File {}
65+
pub struct File {
66+
pub top_level_with: RatchetState<DoesNotIntroduceToplevelWiths>,
67+
}
6668

6769
impl File {
6870
/// Validates the ratchet checks for a top-level package
69-
pub fn compare(
70-
_name: &RelativePath,
71-
_optional_from: Option<&Self>,
72-
_to: &Self,
73-
) -> Validation<()> {
74-
Success(())
71+
pub fn compare(name: &RelativePath, optional_from: Option<&Self>, to: &Self) -> Validation<()> {
72+
validation::sequence_([RatchetState::compare(
73+
name.as_str(),
74+
optional_from.map(|x| &x.top_level_with),
75+
&to.top_level_with,
76+
)])
7577
}
7678
}
7779

@@ -191,3 +193,16 @@ impl ToProblem for UsesByName {
191193
}
192194
}
193195
}
196+
197+
// The ratchet value of an attribute for the check that new nixpkgs changes do not
198+
// introduce top level with or withs that could shadow scope.
199+
200+
pub enum DoesNotIntroduceToplevelWiths {}
201+
202+
impl ToProblem for DoesNotIntroduceToplevelWiths {
203+
type ToContext = Problem;
204+
205+
fn to_problem(_name: &str, _optional_from: Option<()>, to: &Self::ToContext) -> Problem {
206+
to.clone()
207+
}
208+
}

tests/nowith-basic/expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Validated successfully
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import <test-nixpkgs> { root = ./.; }

tests/nowith-basic/main/test.nix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
with lib;
2+
3+
2
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- test.nix: Top level with is discouraged as it may shadow variables and break static checks.
2+
This PR introduces additional instances of discouraged patterns as listed above. Merging is discouraged but would not break the base branch.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import <test-nixpkgs> { root = ./.; }
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
stdenv,
3+
lib,
4+
}:
5+
6+
stdenv.mkDerivation {
7+
pname = "test";
8+
version = "1.0";
9+
10+
src = ./.;
11+
12+
meta = with lib; {
13+
maintainers = [ maintainers.johndoe ];
14+
};
15+
}

0 commit comments

Comments
 (0)