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,79 @@ 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+ /// Files with fewer than this many non-trivia tokens are exempt from the `with` check entirely.
23+ /// Small files don't benefit much from restricting `with` scope.
24+ const WITH_FILE_MIN_TOKENS : usize = 50 ;
25+
26+ /// Counts the non-trivia (non-whitespace, non-comment) tokens under a syntax node.
27+ fn count_non_trivia_tokens ( node : & rnix:: SyntaxNode ) -> usize {
28+ node. descendants_with_tokens ( )
29+ . filter ( |element| element. as_token ( ) . is_some_and ( |t| !t. kind ( ) . is_trivia ( ) ) )
30+ . count ( )
31+ }
32+
33+ /// Finds the first `with` expression in the syntax tree that is overly broad, meaning it either:
34+ ///
35+ /// - Contains more than [`WITH_MAX_TOKENS`] non-trivia tokens, or
36+ /// - Covers more than [`WITH_MAX_FILE_FRACTION`] of the file's total non-trivia tokens.
37+ ///
38+ /// Files with fewer than [`WITH_FILE_MIN_TOKENS`] non-trivia tokens are exempt.
39+ ///
40+ /// Large `with` scopes shadow variables across a wide region, making static analysis unreliable
41+ /// and code harder to understand. Small, tightly-scoped uses (e.g. `with lib.maintainers; [...]`)
42+ /// are fine.
43+ ///
44+ /// Returns `Some(node)` for the first offending `with` node, or `None` if no such node exists.
45+ fn find_overly_broad_with ( syntax : & rnix:: SyntaxNode ) -> Option < rnix:: SyntaxNode > {
46+ let file_tokens = count_non_trivia_tokens ( syntax) ;
47+
48+ if file_tokens < WITH_FILE_MIN_TOKENS {
49+ return None ;
50+ }
51+
52+ syntax
53+ . descendants ( )
54+ . filter ( |node| node. kind ( ) == SyntaxKind :: NODE_WITH )
55+ . find ( |node| {
56+ let with_tokens = count_non_trivia_tokens ( node) ;
57+ with_tokens > WITH_MAX_TOKENS
58+ || with_tokens as f64 > WITH_MAX_FILE_FRACTION * file_tokens as f64
59+ } )
60+ }
61+
62+ /// Runs ratchet checks on all Nix files in the Nixpkgs tree, returning a ratchet result for each.
1263pub fn check_files (
1364 nixpkgs_path : & Path ,
1465 nix_file_store : & mut NixFileStore ,
1566) -> 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 { } ) )
67+ process_nix_files ( nixpkgs_path, nix_file_store, |nix_file| {
68+ Ok ( Success ( ratchet:: File {
69+ top_level_with : check_top_level_with ( nixpkgs_path, nix_file) ,
70+ } ) )
1971 } )
2072}
2173
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 (
74+ /// Checks a single Nix file for top-level `with` expressions that contain nested scope-defining
75+ /// constructs. Returns [`RatchetState::Loose`] with a problem if such a `with` is found, or
76+ /// [`RatchetState::Tight`] if the file is clean.
77+ fn check_top_level_with (
2578 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- } ) )
79+ nix_file : & nix_file:: NixFile ,
80+ ) -> RatchetState < ratchet:: DoesNotIntroduceToplevelWiths > {
81+ if find_overly_broad_with ( nix_file. syntax_root . syntax ( ) ) . is_some ( ) {
82+ let relative_path =
83+ RelativePathBuf :: from_path ( nix_file. path . clone ( ) . strip_prefix ( nixpkgs_path) . unwrap ( ) )
84+ . unwrap ( ) ;
85+ RatchetState :: Loose ( npv_169:: OverlyBroadWith :: new ( relative_path) . into ( ) )
86+ } else {
87+ RatchetState :: Tight
88+ }
5189}
5290
5391/// Recursively collects all Nix files in the relative `dir` within `base`
@@ -63,7 +101,7 @@ fn collect_nix_files(
63101
64102 let absolute_path = entry. path ( ) ;
65103
66- // We'll get to every file based on directory recursion, no need to follow symlinks.
104+ // We reach every file via directory recursion, no need to follow symlinks.
67105 if absolute_path. is_symlink ( ) {
68106 continue ;
69107 }
@@ -75,3 +113,30 @@ fn collect_nix_files(
75113 }
76114 Ok ( ( ) )
77115}
116+
117+ /// Processes all Nix files in a Nixpkgs directory according to a given function `f`, collecting the
118+ /// results into a mapping from each file to a ratchet value.
119+ fn process_nix_files < F : Fn ( & nix_file:: NixFile ) -> validation:: Result < ratchet:: File > > (
120+ nixpkgs_path : & Path ,
121+ nix_file_store : & mut NixFileStore ,
122+ f : F ,
123+ ) -> validation:: Result < BTreeMap < RelativePathBuf , ratchet:: File > > {
124+ // Get all Nix files
125+ let files = {
126+ let mut files = vec ! [ ] ;
127+ collect_nix_files ( nixpkgs_path, & RelativePathBuf :: new ( ) , & mut files) ?;
128+ files
129+ } ;
130+
131+ let file_results: Vec < validation:: Validation < ( RelativePathBuf , ratchet:: File ) > > = files
132+ . into_iter ( )
133+ . map ( |path| {
134+ // Get the (optionally-cached) parsed Nix file
135+ let nix_file = nix_file_store. get ( & path. to_path ( nixpkgs_path) ) ?;
136+ let val = f ( nix_file) ?. map ( |file| ( path, file) ) ;
137+ Ok :: < _ , anyhow:: Error > ( val)
138+ } )
139+ . collect_vec ( ) ?;
140+
141+ Ok ( validation:: sequence ( file_results) . map ( |entries| entries. into_iter ( ) . collect ( ) ) )
142+ }
0 commit comments