diff --git a/src/files.rs b/src/files.rs index 30f93902..112b18fd 100644 --- a/src/files.rs +++ b/src/files.rs @@ -1,5 +1,10 @@ +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; @@ -8,46 +13,77 @@ use crate::validation::ResultIteratorExt; use crate::validation::Validation::Success; use crate::{nix_file, ratchet, structure, validation}; -/// Runs check on all Nix files, returning a ratchet result for each +/// The maximum number of non-trivia tokens allowed under a single `with` expression. +const WITH_MAX_TOKENS: usize = 125; + +/// The maximum fraction of a file's non-trivia tokens that a single `with` expression may cover. +const WITH_MAX_FILE_FRACTION: f64 = 0.25; + +/// Files with fewer than this many non-trivia tokens are exempt from the `with` check entirely. +/// Small files don't benefit much from restricting `with` scope. +const WITH_FILE_MIN_TOKENS: usize = 50; + +/// Counts the non-trivia (non-whitespace, non-comment) tokens under a syntax node. +fn count_non_trivia_tokens(node: &rnix::SyntaxNode) -> usize { + node.descendants_with_tokens() + .filter(|element| element.as_token().is_some_and(|t| !t.kind().is_trivia())) + .count() +} + +/// Finds the first `with` expression in the syntax tree that is overly broad, meaning it either: +/// +/// - Contains more than [`WITH_MAX_TOKENS`] non-trivia tokens, or +/// - Covers more than [`WITH_MAX_FILE_FRACTION`] of the file's total non-trivia tokens. +/// +/// Files with fewer than [`WITH_FILE_MIN_TOKENS`] non-trivia tokens are exempt. +/// +/// Large `with` scopes shadow variables across a wide region, making static analysis unreliable +/// and code harder to understand. Small, tightly-scoped uses (e.g. `with lib.maintainers; [...]`) +/// are fine. +/// +/// Returns `Some(node)` for the first offending `with` node, or `None` if no such node exists. +fn find_overly_broad_with(syntax: &rnix::SyntaxNode) -> Option { + let file_tokens = count_non_trivia_tokens(syntax); + + if file_tokens < WITH_FILE_MIN_TOKENS { + return None; + } + + syntax + .descendants() + .filter(|node| node.kind() == SyntaxKind::NODE_WITH) + .find(|node| { + let with_tokens = count_non_trivia_tokens(node); + with_tokens > WITH_MAX_TOKENS + || with_tokens as f64 > WITH_MAX_FILE_FRACTION * file_tokens as f64 + }) +} + +/// Runs ratchet checks on all Nix files in the Nixpkgs tree, returning a ratchet result for each. pub fn check_files( nixpkgs_path: &Path, nix_file_store: &mut NixFileStore, ) -> validation::Result> { - process_nix_files(nixpkgs_path, nix_file_store, |_nix_file| { - // Noop for now, only boilerplate to make it easier to add future file-based checks - Ok(Success(ratchet::File {})) + process_nix_files(nixpkgs_path, nix_file_store, |relative_path, nix_file| { + Ok(Success(ratchet::File { + top_level_with: check_top_level_with(relative_path, nix_file), + })) }) } -/// Processes all Nix files in a Nixpkgs directory according to a given function `f`, collecting the -/// results into a mapping from each file to a ratchet value. -fn process_nix_files( - nixpkgs_path: &Path, - nix_file_store: &mut NixFileStore, - f: impl Fn(&nix_file::NixFile) -> validation::Result, -) -> validation::Result> { - // Get all Nix files - let files = { - let mut files = vec![]; - collect_nix_files(nixpkgs_path, &RelativePathBuf::new(), &mut files)?; - files - }; - - let results = files - .into_iter() - .map(|path| { - // Get the (optionally-cached) parsed Nix file - let nix_file = nix_file_store.get(&path.to_path(nixpkgs_path))?; - let result = f(nix_file)?; - let val = result.map(|ratchet| (path, ratchet)); - Ok::<_, anyhow::Error>(val) - }) - .collect_vec()?; - - Ok(validation::sequence(results).map(|entries| { - // Convert the Vec to a BTreeMap - entries.into_iter().collect() - })) +/// Checks a single Nix file for overly broad `with` expressions. Returns [`RatchetState::Loose`] +/// with a problem if such a `with` is found, or [`RatchetState::Tight`] if the file is clean. +fn check_top_level_with( + relative_path: &RelativePath, + nix_file: &nix_file::NixFile, +) -> RatchetState { + if find_overly_broad_with(nix_file.syntax_root.syntax()).is_some() { + RatchetState::Loose( + npv_169::OverlyBroadWith::new(relative_path.to_relative_path_buf()).into(), + ) + } else { + RatchetState::Tight + } } /// Recursively collects all Nix files in the relative `dir` within `base` @@ -63,7 +99,7 @@ fn collect_nix_files( let absolute_path = entry.path(); - // We'll get to every file based on directory recursion, no need to follow symlinks. + // We reach every file via directory recursion, no need to follow symlinks. if absolute_path.is_symlink() { continue; } @@ -75,3 +111,30 @@ fn collect_nix_files( } Ok(()) } + +/// Processes all Nix files in a Nixpkgs directory according to a given function `f`, collecting the +/// results into a mapping from each file to a ratchet value. +fn process_nix_files( + nixpkgs_path: &Path, + nix_file_store: &mut NixFileStore, + f: impl Fn(&RelativePath, &nix_file::NixFile) -> validation::Result, +) -> validation::Result> { + // Get all Nix files + let files = { + let mut files = vec![]; + collect_nix_files(nixpkgs_path, &RelativePathBuf::new(), &mut files)?; + files + }; + + let file_results: Vec> = files + .into_iter() + .map(|path| { + // Get the (optionally-cached) parsed Nix file + let nix_file = nix_file_store.get(&path.to_path(nixpkgs_path))?; + let val = f(&path, nix_file)?.map(|file| (path, file)); + Ok::<_, anyhow::Error>(val) + }) + .collect_vec()?; + + Ok(validation::sequence(file_results).map(|entries| entries.into_iter().collect())) +} diff --git a/src/problem/mod.rs b/src/problem/mod.rs index 5a0a79e3..210042df 100644 --- a/src/problem/mod.rs +++ b/src/problem/mod.rs @@ -36,6 +36,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 @@ -131,6 +133,9 @@ pub enum Problem { NewTopLevelPackageShouldBeByNameWithCustomArgument( npv_163::NewTopLevelPackageShouldBeByNameWithCustomArgument, ), + + /// NPV-169: `with` expression covers too much of the file + OverlyBroadWith(npv_169::OverlyBroadWith), } fn indent_definition(column: usize, definition: &str) -> String { diff --git a/src/problem/npv_169.rs b/src/problem/npv_169.rs new file mode 100644 index 00000000..6950c75f --- /dev/null +++ b/src/problem/npv_169.rs @@ -0,0 +1,32 @@ +use std::fmt; + +use indoc::writedoc; +use relative_path::RelativePathBuf; + +#[derive(Clone)] +pub struct OverlyBroadWith { + file: RelativePathBuf, +} + +impl OverlyBroadWith { + pub fn new(file: RelativePathBuf) -> Self { + Self { file } + } +} + +impl fmt::Display for OverlyBroadWith { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let Self { file } = self; + writedoc!( + f, + " + - {file}: `with` expression covers too much of the file. + Large-scoped `with` expressions shadow bindings and make static analysis unreliable. + Prefer one of these alternatives: + - Use fully qualified names, e.g. `lib.mkOption` instead of `with lib; mkOption` + - Use `inherit (lib) mkOption mkIf;` in a `let` block + - Limit `with` to small scopes, e.g. `maintainers = with lib.maintainers; [ ... ]` + ", + ) + } +} diff --git a/src/ratchet.rs b/src/ratchet.rs index e977bb0c..ce1898e3 100644 --- a/src/ratchet.rs +++ b/src/ratchet.rs @@ -62,16 +62,21 @@ impl Package { } } -pub struct File {} +/// The ratchet value for a single Nix file in the Nixpkgs tree. +pub struct File { + /// The ratchet value for the check that files do not introduce top-level `with` expressions + /// that shadow scope. + pub top_level_with: RatchetState, +} impl File { - /// Validates the ratchet checks for a top-level package - pub fn compare( - _name: &RelativePath, - _optional_from: Option<&Self>, - _to: &Self, - ) -> Validation<()> { - Success(()) + /// Validates the ratchet checks for a Nix file. + 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, + )]) } } @@ -191,3 +196,20 @@ impl ToProblem for UsesByName { } } } + +/// The ratchet value for the check that Nix files do not introduce top-level `with` expressions +/// whose body contains nested scope-defining constructs (other `with`s, `let...in`, or attribute +/// sets). +/// +/// Such `with` expressions shadow variables in the enclosing scope, making static analysis +/// unreliable. This ratchet is tight when a file has no such `with` expressions, and loose when +/// one is found. +pub enum DoesNotIntroduceToplevelWiths {} + +impl ToProblem for DoesNotIntroduceToplevelWiths { + type ToContext = Problem; + + fn to_problem(_name: &str, _optional_from: Option<()>, to: &Self::ToContext) -> Problem { + to.clone() + } +} diff --git a/tests/nowith-absolute-cap/expected b/tests/nowith-absolute-cap/expected new file mode 100644 index 00000000..ad6b5f4d --- /dev/null +++ b/tests/nowith-absolute-cap/expected @@ -0,0 +1,8 @@ +- test.nix: `with` expression covers too much of the file. + Large-scoped `with` expressions shadow bindings and make static analysis unreliable. + Prefer one of these alternatives: + - Use fully qualified names, e.g. `lib.mkOption` instead of `with lib; mkOption` + - Use `inherit (lib) mkOption mkIf;` in a `let` block + - Limit `with` to small scopes, e.g. `maintainers = with lib.maintainers; [ ... ]` + +This PR introduces additional instances of discouraged patterns as listed above. Merging is discouraged but would not break the base branch. diff --git a/tests/nowith-absolute-cap/main/default.nix b/tests/nowith-absolute-cap/main/default.nix new file mode 100644 index 00000000..861260cd --- /dev/null +++ b/tests/nowith-absolute-cap/main/default.nix @@ -0,0 +1 @@ +import { root = ./.; } diff --git a/tests/nowith-absolute-cap/main/test.nix b/tests/nowith-absolute-cap/main/test.nix new file mode 100644 index 00000000..ba00a11c --- /dev/null +++ b/tests/nowith-absolute-cap/main/test.nix @@ -0,0 +1,159 @@ +{ + config, + lib, + pkgs, +}: + +let + cfg = config.services.bigmodule; + + configFile = lib.generators.toJSON { } cfg.settings; + + mkSystemdService = name: opts: { + description = "${name} worker"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + ExecStart = "${lib.getExe cfg.package} --worker ${name} --config ${configFile}"; + User = cfg.user; + Group = cfg.group; + StateDirectory = "bigmodule/${name}"; + RuntimeDirectory = "bigmodule/${name}"; + Restart = "on-failure"; + RestartSec = opts.restartSec or 5; + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [ "${cfg.dataDir}/${name}" ]; + LimitNOFILE = opts.maxFds or 65536; + MemoryMax = opts.memoryMax or "2G"; + CPUQuota = opts.cpuQuota or "200%"; + }; + }; +in +{ + # The `with lib;` block here has >125 non-trivia tokens, but the file is large + # enough that the fraction stays under 25%. This tests the absolute token cap. + options.services.bigmodule = with lib; { + enable = mkEnableOption "bigmodule service"; + + package = mkPackageOption pkgs "bigmodule" { }; + + settings = mkOption { + type = types.submodule { + options = { + host = mkOption { + type = types.str; + default = "127.0.0.1"; + }; + + port = mkOption { + type = types.port; + default = 8080; + }; + + workers = mkOption { + type = types.ints.positive; + default = 4; + }; + }; + }; + default = { }; + }; + + user = mkOption { + type = types.str; + default = "bigmodule"; + }; + + group = mkOption { + type = types.str; + default = "bigmodule"; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/bigmodule"; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + }; + }; + + config = lib.mkIf cfg.enable { + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = cfg.dataDir; + createHome = true; + }; + + users.groups.${cfg.group} = { }; + + systemd.services.bigmodule-main = mkSystemdService "main" { + restartSec = 5; + maxFds = 65536; + memoryMax = "4G"; + cpuQuota = "400%"; + }; + + systemd.services.bigmodule-scheduler = mkSystemdService "scheduler" { + restartSec = 10; + maxFds = 1024; + memoryMax = "512M"; + cpuQuota = "100%"; + }; + + systemd.services.bigmodule-cleanup = mkSystemdService "cleanup" { + restartSec = 60; + maxFds = 256; + memoryMax = "256M"; + cpuQuota = "50%"; + }; + + systemd.services.bigmodule-monitor = mkSystemdService "monitor" { + restartSec = 30; + maxFds = 512; + memoryMax = "128M"; + cpuQuota = "25%"; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0750 ${cfg.user} ${cfg.group} -" + "d ${cfg.dataDir}/main 0750 ${cfg.user} ${cfg.group} -" + "d ${cfg.dataDir}/scheduler 0750 ${cfg.user} ${cfg.group} -" + "d ${cfg.dataDir}/cleanup 0750 ${cfg.user} ${cfg.group} -" + "d ${cfg.dataDir}/monitor 0750 ${cfg.user} ${cfg.group} -" + "d /run/bigmodule 0750 ${cfg.user} ${cfg.group} -" + ]; + + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ + cfg.settings.port + ]; + + environment.etc."bigmodule/config.json".source = configFile; + + environment.systemPackages = [ cfg.package ]; + + assertions = [ + { + assertion = cfg.settings.workers >= 1; + message = "bigmodule requires at least 1 worker"; + } + { + assertion = cfg.settings.port > 0; + message = "bigmodule port must be positive"; + } + { + assertion = cfg.user != "root"; + message = "bigmodule should not run as root"; + } + { + assertion = cfg.dataDir != "/"; + message = "bigmodule data directory must not be root"; + } + ]; + }; +} diff --git a/tests/nowith-fraction-rule/expected b/tests/nowith-fraction-rule/expected new file mode 100644 index 00000000..ad6b5f4d --- /dev/null +++ b/tests/nowith-fraction-rule/expected @@ -0,0 +1,8 @@ +- test.nix: `with` expression covers too much of the file. + Large-scoped `with` expressions shadow bindings and make static analysis unreliable. + Prefer one of these alternatives: + - Use fully qualified names, e.g. `lib.mkOption` instead of `with lib; mkOption` + - Use `inherit (lib) mkOption mkIf;` in a `let` block + - Limit `with` to small scopes, e.g. `maintainers = with lib.maintainers; [ ... ]` + +This PR introduces additional instances of discouraged patterns as listed above. Merging is discouraged but would not break the base branch. diff --git a/tests/nowith-fraction-rule/main/default.nix b/tests/nowith-fraction-rule/main/default.nix new file mode 100644 index 00000000..861260cd --- /dev/null +++ b/tests/nowith-fraction-rule/main/default.nix @@ -0,0 +1 @@ +import { root = ./.; } diff --git a/tests/nowith-fraction-rule/main/test.nix b/tests/nowith-fraction-rule/main/test.nix new file mode 100644 index 00000000..b3521b48 --- /dev/null +++ b/tests/nowith-fraction-rule/main/test.nix @@ -0,0 +1,34 @@ +{ + config, + lib, + pkgs, +}: + +let + cfg = config.services.example; +in +{ + options.services.example = with lib; { + enable = mkEnableOption "example service"; + + package = mkPackageOption pkgs "example" { }; + + settings = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Configuration for example."; + }; + + user = mkOption { + type = types.str; + default = "example"; + description = "User account under which example runs."; + }; + + group = mkOption { + type = types.str; + default = "example"; + description = "Group under which example runs."; + }; + }; +} diff --git a/tests/nowith-new-broad-with-fails/expected b/tests/nowith-new-broad-with-fails/expected new file mode 100644 index 00000000..ad6b5f4d --- /dev/null +++ b/tests/nowith-new-broad-with-fails/expected @@ -0,0 +1,8 @@ +- test.nix: `with` expression covers too much of the file. + Large-scoped `with` expressions shadow bindings and make static analysis unreliable. + Prefer one of these alternatives: + - Use fully qualified names, e.g. `lib.mkOption` instead of `with lib; mkOption` + - Use `inherit (lib) mkOption mkIf;` in a `let` block + - Limit `with` to small scopes, e.g. `maintainers = with lib.maintainers; [ ... ]` + +This PR introduces additional instances of discouraged patterns as listed above. Merging is discouraged but would not break the base branch. diff --git a/tests/nowith-new-broad-with-fails/main/default.nix b/tests/nowith-new-broad-with-fails/main/default.nix new file mode 100644 index 00000000..861260cd --- /dev/null +++ b/tests/nowith-new-broad-with-fails/main/default.nix @@ -0,0 +1 @@ +import { root = ./.; } diff --git a/tests/nowith-new-broad-with-fails/main/test.nix b/tests/nowith-new-broad-with-fails/main/test.nix new file mode 100644 index 00000000..00bf57c6 --- /dev/null +++ b/tests/nowith-new-broad-with-fails/main/test.nix @@ -0,0 +1,39 @@ +{ + config, + lib, +}: + +let + cfg = config.foo; +in +{ + options.foo = lib.mkOption { + type = # random example from nixpkgs + with lib.types; + attrsOf ( + either path (submodule { + options = { + service = lib.mkOption { + type = nullOr str; + default = null; + description = "The service on which to perform \ after fetching."; + }; + + action = lib.mkOption { + type = addCheck str ( + x: + cfg.svcManager == "command" + || lib.elem x [ + "restart" + "reload" + "nop" + ] + ); + default = "nop"; + description = "The action to take after fetching."; + }; + }; + }) + ); + }; +} diff --git a/tests/nowith-ratchet-fixed/expected b/tests/nowith-ratchet-fixed/expected new file mode 100644 index 00000000..defae263 --- /dev/null +++ b/tests/nowith-ratchet-fixed/expected @@ -0,0 +1 @@ +Validated successfully diff --git a/tests/nowith-ratchet-fixed/main/default.nix b/tests/nowith-ratchet-fixed/main/default.nix new file mode 100644 index 00000000..861260cd --- /dev/null +++ b/tests/nowith-ratchet-fixed/main/default.nix @@ -0,0 +1 @@ +import { root = ./.; } diff --git a/tests/nowith-ratchet-fixed/main/test.nix b/tests/nowith-ratchet-fixed/main/test.nix new file mode 100644 index 00000000..a91b7af1 --- /dev/null +++ b/tests/nowith-ratchet-fixed/main/test.nix @@ -0,0 +1,53 @@ +{ + config, + lib, +}: + +let + cfg = config.foo; + + inherit (lib) + mkOption + elem + ; + + inherit (lib.types) + attrsOf + either + path + submodule + nullOr + str + addCheck + ; +in +{ + options.foo = mkOption { + type = # random example from nixpkgs + attrsOf ( + either path (submodule { + options = { + service = mkOption { + type = nullOr str; + default = null; + description = "The service on which to perform \ after fetching."; + }; + + action = mkOption { + type = addCheck str ( + x: + cfg.svcManager == "command" + || elem x [ + "restart" + "reload" + "nop" + ] + ); + default = "nop"; + description = "The action to take after fetching."; + }; + }; + }) + ); + }; +} diff --git a/tests/nowith-ratchet-grandfathered/base/default.nix b/tests/nowith-ratchet-grandfathered/base/default.nix new file mode 100644 index 00000000..861260cd --- /dev/null +++ b/tests/nowith-ratchet-grandfathered/base/default.nix @@ -0,0 +1 @@ +import { root = ./.; } diff --git a/tests/nowith-ratchet-grandfathered/base/test.nix b/tests/nowith-ratchet-grandfathered/base/test.nix new file mode 100644 index 00000000..00bf57c6 --- /dev/null +++ b/tests/nowith-ratchet-grandfathered/base/test.nix @@ -0,0 +1,39 @@ +{ + config, + lib, +}: + +let + cfg = config.foo; +in +{ + options.foo = lib.mkOption { + type = # random example from nixpkgs + with lib.types; + attrsOf ( + either path (submodule { + options = { + service = lib.mkOption { + type = nullOr str; + default = null; + description = "The service on which to perform \ after fetching."; + }; + + action = lib.mkOption { + type = addCheck str ( + x: + cfg.svcManager == "command" + || lib.elem x [ + "restart" + "reload" + "nop" + ] + ); + default = "nop"; + description = "The action to take after fetching."; + }; + }; + }) + ); + }; +} diff --git a/tests/nowith-ratchet-grandfathered/expected b/tests/nowith-ratchet-grandfathered/expected new file mode 100644 index 00000000..defae263 --- /dev/null +++ b/tests/nowith-ratchet-grandfathered/expected @@ -0,0 +1 @@ +Validated successfully diff --git a/tests/nowith-ratchet-grandfathered/main/default.nix b/tests/nowith-ratchet-grandfathered/main/default.nix new file mode 100644 index 00000000..861260cd --- /dev/null +++ b/tests/nowith-ratchet-grandfathered/main/default.nix @@ -0,0 +1 @@ +import { root = ./.; } diff --git a/tests/nowith-ratchet-grandfathered/main/test.nix b/tests/nowith-ratchet-grandfathered/main/test.nix new file mode 100644 index 00000000..25c9f006 --- /dev/null +++ b/tests/nowith-ratchet-grandfathered/main/test.nix @@ -0,0 +1,42 @@ +{ + config, + lib, +}: + +let + cfg = config.foo; +in +{ + options.bar = lib.mkOption { + type = lib.types.str; + }; + options.foo = lib.mkOption { + type = # random example from nixpkgs + with lib.types; + attrsOf ( + either path (submodule { + options = { + service = lib.mkOption { + type = nullOr str; + default = null; + description = "The service on which to perform \ after fetching."; + }; + + action = lib.mkOption { + type = addCheck str ( + x: + cfg.svcManager == "command" + || lib.elem x [ + "restart" + "reload" + "nop" + ] + ); + default = "nop"; + description = "The action to take after fetching."; + }; + }; + }) + ); + }; +} diff --git a/tests/nowith-small-file-exempt/expected b/tests/nowith-small-file-exempt/expected new file mode 100644 index 00000000..defae263 --- /dev/null +++ b/tests/nowith-small-file-exempt/expected @@ -0,0 +1 @@ +Validated successfully diff --git a/tests/nowith-small-file-exempt/main/default.nix b/tests/nowith-small-file-exempt/main/default.nix new file mode 100644 index 00000000..861260cd --- /dev/null +++ b/tests/nowith-small-file-exempt/main/default.nix @@ -0,0 +1 @@ +import { root = ./.; } diff --git a/tests/nowith-small-file-exempt/main/test.nix b/tests/nowith-small-file-exempt/main/test.nix new file mode 100644 index 00000000..2c1be06d --- /dev/null +++ b/tests/nowith-small-file-exempt/main/test.nix @@ -0,0 +1,3 @@ +with lib; + +2 diff --git a/tests/nowith-small-scope-ok/expected b/tests/nowith-small-scope-ok/expected new file mode 100644 index 00000000..defae263 --- /dev/null +++ b/tests/nowith-small-scope-ok/expected @@ -0,0 +1 @@ +Validated successfully diff --git a/tests/nowith-small-scope-ok/main/default.nix b/tests/nowith-small-scope-ok/main/default.nix new file mode 100644 index 00000000..861260cd --- /dev/null +++ b/tests/nowith-small-scope-ok/main/default.nix @@ -0,0 +1 @@ +import { root = ./.; } diff --git a/tests/nowith-small-scope-ok/main/test.nix b/tests/nowith-small-scope-ok/main/test.nix new file mode 100644 index 00000000..920b0e17 --- /dev/null +++ b/tests/nowith-small-scope-ok/main/test.nix @@ -0,0 +1,15 @@ +{ + stdenv, + lib, +}: + +stdenv.mkDerivation { + pname = "test"; + version = "1.0"; + + src = ./.; + + meta = { + maintainers = with lib.maintainers; [ johndoe ]; + }; +}