1+ use crate :: problem:: npv_169;
2+ use crate :: ratchet:: RatchetState ;
13use relative_path:: RelativePath ;
24use relative_path:: RelativePathBuf ;
5+
6+ use rnix:: SyntaxKind ;
7+ use rowan:: ast:: AstNode ;
38use std:: collections:: BTreeMap ;
49use std:: path:: Path ;
510
@@ -8,46 +13,75 @@ use crate::validation::ResultIteratorExt;
813use crate :: validation:: Validation :: Success ;
914use crate :: { nix_file, ratchet, structure, validation} ;
1015
11- /// Runs check on all Nix files, returning a ratchet result for each
16+ /// The maximum number of non-trivia tokens allowed under a single `with` expression.
17+ const WITH_MAX_TOKENS : usize = 125 ;
18+
19+ /// The maximum fraction of a file's non-trivia tokens that a single `with` expression may cover.
20+ const WITH_MAX_FILE_FRACTION : f64 = 0.25 ;
21+
22+ /// Counts the non-trivia (non-whitespace, non-comment) tokens under a syntax node.
23+ fn count_non_trivia_tokens ( node : & rnix:: SyntaxNode ) -> usize {
24+ node. descendants_with_tokens ( )
25+ . filter ( |element| element. as_token ( ) . is_some_and ( |t| !t. kind ( ) . is_trivia ( ) ) )
26+ . count ( )
27+ }
28+
29+ /// Finds the first `with` expression in the syntax tree that is overly broad, meaning it either:
30+ ///
31+ /// - Contains more than [`WITH_MAX_TOKENS`] non-trivia tokens, or
32+ /// - Covers more than [`WITH_MAX_FILE_FRACTION`] of the file's total non-trivia tokens.
33+ ///
34+ /// Large `with` scopes shadow variables across a wide region, making static analysis unreliable
35+ /// and code harder to understand. Small, tightly-scoped uses (e.g. `with lib.maintainers; [...]`)
36+ /// are fine.
37+ ///
38+ /// Returns `Some(node)` for the first offending `with` node, or `None` if no such node exists.
39+ fn find_overly_broad_with ( syntax : & rnix:: SyntaxNode ) -> Option < rnix:: SyntaxNode > {
40+ let file_tokens = count_non_trivia_tokens ( syntax) ;
41+
42+ syntax
43+ . descendants ( )
44+ . filter ( |node| node. kind ( ) == SyntaxKind :: NODE_WITH )
45+ . find ( |node| {
46+ let with_tokens = count_non_trivia_tokens ( node) ;
47+ with_tokens > WITH_MAX_TOKENS
48+ || with_tokens as f64 > WITH_MAX_FILE_FRACTION * file_tokens as f64
49+ } )
50+ }
51+
52+ /// Runs ratchet checks on all Nix files in the Nixpkgs tree, returning a ratchet result for each.
1253pub fn check_files (
1354 nixpkgs_path : & Path ,
1455 nix_file_store : & mut NixFileStore ,
1556) -> 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 { } ) )
57+ process_nix_files ( nixpkgs_path, nix_file_store, |nix_file| {
58+ Ok ( Success ( ratchet:: File {
59+ top_level_with : check_top_level_with ( nixpkgs_path, nix_file) ,
60+ } ) )
1961 } )
2062}
2163
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 (
64+ /// Checks a single Nix file for top-level `with` expressions that contain nested scope-defining
65+ /// constructs. Returns [`RatchetState::Loose`] with a problem if such a `with` is found, or
66+ /// [`RatchetState::Tight`] if the file is clean.
67+ fn check_top_level_with (
2568 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- } ) )
69+ nix_file : & nix_file:: NixFile ,
70+ ) -> RatchetState < ratchet:: DoesNotIntroduceToplevelWiths > {
71+ if let Some ( offending_with) = find_overly_broad_with ( nix_file. syntax_root . syntax ( ) ) {
72+ let relative_path =
73+ RelativePathBuf :: from_path ( nix_file. path . clone ( ) . strip_prefix ( nixpkgs_path) . unwrap ( ) )
74+ . unwrap ( ) ;
75+ RatchetState :: Loose (
76+ npv_169:: TopLevelWithMayShadowVariablesAndBreakStaticChecks :: new (
77+ relative_path,
78+ offending_with. to_string ( ) ,
79+ )
80+ . into ( ) ,
81+ )
82+ } else {
83+ RatchetState :: Tight
84+ }
5185}
5286
5387/// Recursively collects all Nix files in the relative `dir` within `base`
@@ -63,7 +97,7 @@ fn collect_nix_files(
6397
6498 let absolute_path = entry. path ( ) ;
6599
66- // We'll get to every file based on directory recursion, no need to follow symlinks.
100+ // We reach every file via directory recursion, no need to follow symlinks.
67101 if absolute_path. is_symlink ( ) {
68102 continue ;
69103 }
@@ -75,3 +109,30 @@ fn collect_nix_files(
75109 }
76110 Ok ( ( ) )
77111}
112+
113+ /// Processes all Nix files in a Nixpkgs directory according to a given function `f`, collecting the
114+ /// results into a mapping from each file to a ratchet value.
115+ fn process_nix_files < F : Fn ( & nix_file:: NixFile ) -> validation:: Result < ratchet:: File > > (
116+ nixpkgs_path : & Path ,
117+ nix_file_store : & mut NixFileStore ,
118+ f : F ,
119+ ) -> validation:: Result < BTreeMap < RelativePathBuf , ratchet:: File > > {
120+ // Get all Nix files
121+ let files = {
122+ let mut files = vec ! [ ] ;
123+ collect_nix_files ( nixpkgs_path, & RelativePathBuf :: new ( ) , & mut files) ?;
124+ files
125+ } ;
126+
127+ let file_results: Vec < validation:: Validation < ( RelativePathBuf , ratchet:: File ) > > = files
128+ . into_iter ( )
129+ . map ( |path| {
130+ // Get the (optionally-cached) parsed Nix file
131+ let nix_file = nix_file_store. get ( & path. to_path ( nixpkgs_path) ) ?;
132+ let val = f ( nix_file) ?. map ( |file| ( path, file) ) ;
133+ Ok :: < _ , anyhow:: Error > ( val)
134+ } )
135+ . collect_vec ( ) ?;
136+
137+ Ok ( validation:: sequence ( file_results) . map ( |entries| entries. into_iter ( ) . collect ( ) ) )
138+ }
0 commit comments