about summary refs log tree commit diff
path: root/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nix_file.rs
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nix_file.rs')
-rw-r--r--nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nix_file.rs555
1 files changed, 0 insertions, 555 deletions
diff --git a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nix_file.rs b/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nix_file.rs
deleted file mode 100644
index e2dc1e196141..000000000000
--- a/nixpkgs/pkgs/test/nixpkgs-check-by-name/src/nix_file.rs
+++ /dev/null
@@ -1,555 +0,0 @@
-//! This is a utility module for interacting with the syntax of Nix files
-
-use crate::utils::LineIndex;
-use anyhow::Context;
-use itertools::Either::{self, Left, Right};
-use relative_path::RelativePathBuf;
-use rnix::ast;
-use rnix::ast::Expr;
-use rnix::ast::HasEntry;
-use rowan::ast::AstNode;
-use rowan::TextSize;
-use rowan::TokenAtOffset;
-use std::collections::hash_map::Entry;
-use std::collections::HashMap;
-use std::fs::read_to_string;
-use std::path::Path;
-use std::path::PathBuf;
-
-/// A structure to store parse results of Nix files in memory,
-/// making sure that the same file never has to be parsed twice
-#[derive(Default)]
-pub struct NixFileStore {
-    entries: HashMap<PathBuf, NixFile>,
-}
-
-impl NixFileStore {
-    /// Get the store entry for a Nix file if it exists, otherwise parse the file, insert it into
-    /// the store, and return the value
-    ///
-    /// Note that this function only gives an anyhow::Result::Err for I/O errors.
-    /// A parse error is anyhow::Result::Ok(Result::Err(error))
-    pub fn get(&mut self, path: &Path) -> anyhow::Result<&NixFile> {
-        match self.entries.entry(path.to_owned()) {
-            Entry::Occupied(entry) => Ok(entry.into_mut()),
-            Entry::Vacant(entry) => Ok(entry.insert(NixFile::new(path)?)),
-        }
-    }
-}
-
-/// A structure for storing a successfully parsed Nix file
-pub struct NixFile {
-    /// The parent directory of the Nix file, for more convenient error handling
-    pub parent_dir: PathBuf,
-    /// The path to the file itself, for errors
-    pub path: PathBuf,
-    pub syntax_root: rnix::Root,
-    pub line_index: LineIndex,
-}
-
-impl NixFile {
-    /// Creates a new NixFile, failing for I/O or parse errors
-    fn new(path: impl AsRef<Path>) -> anyhow::Result<NixFile> {
-        let Some(parent_dir) = path.as_ref().parent() else {
-            anyhow::bail!("Could not get parent of path {}", path.as_ref().display())
-        };
-
-        let contents = read_to_string(&path)
-            .with_context(|| format!("Could not read file {}", path.as_ref().display()))?;
-        let line_index = LineIndex::new(&contents);
-
-        // NOTE: There's now another Nixpkgs CI check to make sure all changed Nix files parse
-        // correctly, though that uses mainline Nix instead of rnix, so it doesn't give the same
-        // errors. In the future we should unify these two checks, ideally moving the other CI
-        // check into this tool as well and checking for both mainline Nix and rnix.
-        rnix::Root::parse(&contents)
-            // rnix's ::ok returns Result<_, _> , so no error is thrown away like it would be with
-            // std::result's ::ok
-            .ok()
-            .map(|syntax_root| NixFile {
-                parent_dir: parent_dir.to_path_buf(),
-                path: path.as_ref().to_owned(),
-                syntax_root,
-                line_index,
-            })
-            .with_context(|| format!("Could not parse file {} with rnix", path.as_ref().display()))
-    }
-}
-
-/// Information about callPackage arguments
-#[derive(Debug, PartialEq)]
-pub struct CallPackageArgumentInfo {
-    /// The relative path of the first argument, or `None` if it's not a path.
-    pub relative_path: Option<RelativePathBuf>,
-    /// Whether the second argument is an empty attribute set
-    pub empty_arg: bool,
-}
-
-impl NixFile {
-    /// Returns information about callPackage arguments for an attribute at a specific line/column
-    /// index.
-    /// If the definition at the given location is not of the form `<attr> = callPackage <arg1> <arg2>;`,
-    /// `Ok((None, String))` is returned, with `String` being the definition itself.
-    ///
-    /// This function only returns `Err` for problems that can't be caused by the Nix contents,
-    /// but rather problems in this programs code itself.
-    ///
-    /// This is meant to be used with the location returned from `builtins.unsafeGetAttrPos`, e.g.:
-    /// - Create file `default.nix` with contents
-    ///   ```nix
-    ///   self: {
-    ///     foo = self.callPackage ./default.nix { };
-    ///   }
-    ///   ```
-    /// - Evaluate
-    ///   ```nix
-    ///   builtins.unsafeGetAttrPos "foo" (import ./default.nix { })
-    ///   ```
-    ///   results in `{ file = ./default.nix; line = 2; column = 3; }`
-    /// - Get the NixFile for `.file` from a `NixFileStore`
-    /// - Call this function with `.line`, `.column` and `relative_to` as the (absolute) current directory
-    ///
-    /// You'll get back
-    /// ```rust
-    /// Ok((
-    ///   Some(CallPackageArgumentInfo { path = Some("default.nix"), empty_arg: true }),
-    ///   "foo = self.callPackage ./default.nix { };",
-    /// ))
-    /// ```
-    ///
-    /// Note that this also returns the same for `pythonPackages.callPackage`. It doesn't make an
-    /// attempt at distinguishing this.
-    pub fn call_package_argument_info_at(
-        &self,
-        line: usize,
-        column: usize,
-        relative_to: &Path,
-    ) -> anyhow::Result<(Option<CallPackageArgumentInfo>, String)> {
-        Ok(match self.attrpath_value_at(line, column)? {
-            Left(definition) => (None, definition),
-            Right(attrpath_value) => {
-                let definition = attrpath_value.to_string();
-                let attrpath_value =
-                    self.attrpath_value_call_package_argument_info(attrpath_value, relative_to)?;
-                (attrpath_value, definition)
-            }
-        })
-    }
-
-    // Internal function mainly to make it independently testable
-    fn attrpath_value_at(
-        &self,
-        line: usize,
-        column: usize,
-    ) -> anyhow::Result<Either<String, ast::AttrpathValue>> {
-        let index = self.line_index.fromlinecolumn(line, column);
-
-        let token_at_offset = self
-            .syntax_root
-            .syntax()
-            .token_at_offset(TextSize::from(index as u32));
-
-        // The token_at_offset function takes indices to mean a location _between_ characters,
-        // which in this case is some spacing followed by the attribute name:
-        //
-        //   foo = 10;
-        //  /\
-        //  This is the token offset, we get both the (newline + indentation) on the left side,
-        //  and the attribute name on the right side.
-        let TokenAtOffset::Between(_space, token) = token_at_offset else {
-            anyhow::bail!("Line {line} column {column} in {} is not the start of a token, but rather {token_at_offset:?}", self.path.display())
-        };
-
-        // token looks like "foo"
-        let Some(node) = token.parent() else {
-            anyhow::bail!(
-                "Token on line {line} column {column} in {} does not have a parent node: {token:?}",
-                self.path.display()
-            )
-        };
-
-        if ast::Attr::can_cast(node.kind()) {
-            // Something like `foo`, `"foo"` or `${"foo"}`
-        } else if ast::Inherit::can_cast(node.kind()) {
-            // Something like `inherit <attr>` or `inherit (<source>) <attr>`
-            // This is the only other way how `builtins.unsafeGetAttrPos` can return
-            // attribute positions, but we only look for ones like `<attr-path> = <value>`, so
-            // ignore this
-            return Ok(Left(node.to_string()));
-        } else {
-            // However, anything else is not expected and smells like a bug
-            anyhow::bail!(
-                "Node in {} is neither an attribute node nor an inherit node: {node:?}",
-                self.path.display()
-            )
-        }
-
-        // node looks like "foo"
-        let Some(attrpath_node) = node.parent() else {
-            anyhow::bail!(
-                "Node in {} does not have a parent node: {node:?}",
-                self.path.display()
-            )
-        };
-
-        if !ast::Attrpath::can_cast(attrpath_node.kind()) {
-            // We know that `node` is an attribute, its parent should be an attribute path
-            anyhow::bail!(
-                "In {}, attribute parent node is not an attribute path node: {attrpath_node:?}",
-                self.path.display()
-            )
-        }
-
-        // attrpath_node looks like "foo.bar"
-        let Some(attrpath_value_node) = attrpath_node.parent() else {
-            anyhow::bail!(
-                "Attribute path node in {} does not have a parent node: {attrpath_node:?}",
-                self.path.display()
-            )
-        };
-
-        if !ast::AttrpathValue::can_cast(attrpath_value_node.kind()) {
-            anyhow::bail!(
-                "Node in {} is not an attribute path value node: {attrpath_value_node:?}",
-                self.path.display()
-            )
-        }
-        // attrpath_value_node looks like "foo.bar = 10;"
-
-        // unwrap is fine because we confirmed that we can cast with the above check.
-        // We could avoid this `unwrap` for a `clone`, since `cast` consumes the argument,
-        // but we still need it for the error message when the cast fails.
-        Ok(Right(
-            ast::AttrpathValue::cast(attrpath_value_node).unwrap(),
-        ))
-    }
-
-    // Internal function mainly to make attrpath_value_at independently testable
-    fn attrpath_value_call_package_argument_info(
-        &self,
-        attrpath_value: ast::AttrpathValue,
-        relative_to: &Path,
-    ) -> anyhow::Result<Option<CallPackageArgumentInfo>> {
-        let Some(attrpath) = attrpath_value.attrpath() else {
-            anyhow::bail!("attrpath value node doesn't have an attrpath: {attrpath_value:?}")
-        };
-
-        // At this point we know it's something like `foo...bar = ...`
-
-        if attrpath.attrs().count() > 1 {
-            // If the attribute path has multiple entries, the left-most entry is an attribute and
-            // can't be a `callPackage`.
-            //
-            // FIXME: `builtins.unsafeGetAttrPos` will return the same position for all attribute
-            // paths and we can't really know which one it is. We could have a case like
-            // `foo.bar = callPackage ... { }` and trying to determine if `bar` is a `callPackage`,
-            // where this is not correct.
-            // However, this case typically doesn't occur anyways,
-            // because top-level packages wouldn't be nested under an attribute set.
-            return Ok(None);
-        }
-        let Some(value) = attrpath_value.value() else {
-            anyhow::bail!("attrpath value node doesn't have a value: {attrpath_value:?}")
-        };
-
-        // At this point we know it's something like `foo = ...`
-
-        let Expr::Apply(apply1) = value else {
-            // Not even a function call, instead something like `foo = null`
-            return Ok(None);
-        };
-        let Some(function1) = apply1.lambda() else {
-            anyhow::bail!("apply node doesn't have a lambda: {apply1:?}")
-        };
-        let Some(arg1) = apply1.argument() else {
-            anyhow::bail!("apply node doesn't have an argument: {apply1:?}")
-        };
-
-        // At this point we know it's something like `foo = <fun> <arg>`.
-        // For a callPackage, `<fun>` would be `callPackage ./file` and `<arg>` would be `{ }`
-
-        let empty_arg = if let Expr::AttrSet(attrset) = arg1 {
-            // We can only statically determine whether the argument is empty if it's an attribute
-            // set _expression_, even though other kind of expressions could evaluate to an attribute
-            // set _value_. But this is what we want anyways
-            attrset.entries().next().is_none()
-        } else {
-            false
-        };
-
-        // Because callPackage takes two curried arguments, the first function needs to be a
-        // function call itself
-        let Expr::Apply(apply2) = function1 else {
-            // Not a callPackage, instead something like `foo = import ./foo`
-            return Ok(None);
-        };
-        let Some(function2) = apply2.lambda() else {
-            anyhow::bail!("apply node doesn't have a lambda: {apply2:?}")
-        };
-        let Some(arg2) = apply2.argument() else {
-            anyhow::bail!("apply node doesn't have an argument: {apply2:?}")
-        };
-
-        // At this point we know it's something like `foo = <fun2> <arg2> <arg1>`.
-        // For a callPackage, `<fun2>` would be `callPackage`, `<arg2>` would be `./file`
-
-        // Check that <arg2> is a path expression
-        let path = if let Expr::Path(actual_path) = arg2 {
-            // Try to statically resolve the path and turn it into a nixpkgs-relative path
-            if let ResolvedPath::Within(p) = self.static_resolve_path(actual_path, relative_to) {
-                Some(p)
-            } else {
-                // We can't statically know an existing path inside Nixpkgs used as <arg2>
-                None
-            }
-        } else {
-            // <arg2> is not a path, but rather e.g. an inline expression
-            None
-        };
-
-        // Check that <fun2> is an identifier, or an attribute path with an identifier at the end
-        let ident = match function2 {
-            Expr::Ident(ident) => {
-                // This means it's something like `foo = callPackage <arg2> <arg1>`
-                ident
-            }
-            Expr::Select(select) => {
-                // This means it's something like `foo = self.callPackage <arg2> <arg1>`.
-                // We also end up here for e.g. `pythonPackages.callPackage`, but the
-                // callPackage-mocking method will take care of not triggering for this case.
-
-                if select.default_expr().is_some() {
-                    // Very odd case, but this would be `foo = self.callPackage or true ./test.nix {}
-                    // (yes this is valid Nix code)
-                    return Ok(None);
-                }
-                let Some(attrpath) = select.attrpath() else {
-                    anyhow::bail!("select node doesn't have an attrpath: {select:?}")
-                };
-                let Some(last) = attrpath.attrs().last() else {
-                    // This case shouldn't be possible, it would be `foo = self. ./test.nix {}`,
-                    // which shouldn't parse
-                    anyhow::bail!("select node has an empty attrpath: {select:?}")
-                };
-                if let ast::Attr::Ident(ident) = last {
-                    ident
-                } else {
-                    // Here it's something like `foo = self."callPackage" /test.nix {}`
-                    // which we're not gonna bother with
-                    return Ok(None);
-                }
-            }
-            // Any other expression we're not gonna treat as callPackage
-            _ => return Ok(None),
-        };
-
-        let Some(token) = ident.ident_token() else {
-            anyhow::bail!("ident node doesn't have a token: {ident:?}")
-        };
-
-        if token.text() == "callPackage" {
-            Ok(Some(CallPackageArgumentInfo {
-                relative_path: path,
-                empty_arg,
-            }))
-        } else {
-            Ok(None)
-        }
-    }
-}
-
-/// The result of trying to statically resolve a Nix path expression
-pub enum ResolvedPath {
-    /// Something like `./foo/${bar}/baz`, can't be known statically
-    Interpolated,
-    /// Something like `<nixpkgs>`, can't be known statically
-    SearchPath,
-    /// Path couldn't be resolved due to an IO error,
-    /// e.g. if the path doesn't exist or you don't have the right permissions
-    Unresolvable(std::io::Error),
-    /// The path is outside the given absolute path
-    Outside,
-    /// The path is within the given absolute path.
-    /// The `RelativePathBuf` is the relative path under the given absolute path.
-    Within(RelativePathBuf),
-}
-
-impl NixFile {
-    /// Statically resolves a Nix path expression and checks that it's within an absolute path
-    ///
-    /// E.g. for the path expression `./bar.nix` in `./foo.nix` and an absolute path of the
-    /// current directory, the function returns `ResolvedPath::Within(./bar.nix)`
-    pub fn static_resolve_path(&self, node: ast::Path, relative_to: &Path) -> ResolvedPath {
-        if node.parts().count() != 1 {
-            // If there's more than 1 interpolated part, it's of the form `./foo/${bar}/baz`.
-            return ResolvedPath::Interpolated;
-        }
-
-        let text = node.to_string();
-
-        if text.starts_with('<') {
-            // A search path like `<nixpkgs>`. There doesn't appear to be better way to detect
-            // these in rnix
-            return ResolvedPath::SearchPath;
-        }
-
-        // Join the file's parent directory and the path expression, then resolve it
-        // FIXME: Expressions like `../../../../foo/bar/baz/qux` or absolute paths
-        // may resolve close to the original file, but may have left the relative_to.
-        // That should be checked more strictly
-        match self.parent_dir.join(Path::new(&text)).canonicalize() {
-            Err(resolution_error) => ResolvedPath::Unresolvable(resolution_error),
-            Ok(resolved) => {
-                // Check if it's within relative_to
-                match resolved.strip_prefix(relative_to) {
-                    Err(_prefix_error) => ResolvedPath::Outside,
-                    Ok(suffix) => ResolvedPath::Within(
-                        RelativePathBuf::from_path(suffix).expect("a relative path"),
-                    ),
-                }
-            }
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::tests;
-    use indoc::indoc;
-
-    #[test]
-    fn detects_attributes() -> anyhow::Result<()> {
-        let temp_dir = tests::tempdir()?;
-        let file = temp_dir.path().join("file.nix");
-        let contents = indoc! {r#"
-            toInherit: {
-              foo = 1;
-              "bar" = 2;
-              ${"baz"} = 3;
-              "${"qux"}" = 4;
-
-              # A
-              quux
-              # B
-              =
-              # C
-              5
-              # D
-              ;
-              # E
-
-              /**/quuux/**/=/**/5/**/;/*E*/
-
-              inherit toInherit;
-              inherit (toInherit) toInherit;
-            }
-        "#};
-
-        std::fs::write(&file, contents)?;
-
-        let nix_file = NixFile::new(&file)?;
-
-        // These are builtins.unsafeGetAttrPos locations for the attributes
-        let cases = [
-            (2, 3, Right("foo = 1;")),
-            (3, 3, Right(r#""bar" = 2;"#)),
-            (4, 3, Right(r#"${"baz"} = 3;"#)),
-            (5, 3, Right(r#""${"qux"}" = 4;"#)),
-            (8, 3, Right("quux\n  # B\n  =\n  # C\n  5\n  # D\n  ;")),
-            (17, 7, Right("quuux/**/=/**/5/**/;")),
-            (19, 10, Left("inherit toInherit;")),
-            (20, 22, Left("inherit (toInherit) toInherit;")),
-        ];
-
-        for (line, column, expected_result) in cases {
-            let actual_result = nix_file
-                .attrpath_value_at(line, column)
-                .context(format!("line {line}, column {column}"))?
-                .map_right(|node| node.to_string());
-            let owned_expected_result = expected_result
-                .map(|x| x.to_string())
-                .map_left(|x| x.to_string());
-            assert_eq!(
-                actual_result, owned_expected_result,
-                "line {line}, column {column}"
-            );
-        }
-
-        Ok(())
-    }
-
-    #[test]
-    fn detects_call_package() -> anyhow::Result<()> {
-        let temp_dir = tests::tempdir()?;
-        let file = temp_dir.path().join("file.nix");
-        let contents = indoc! {r#"
-            self: with self; {
-              a.sub = null;
-              b = null;
-              c = import ./file.nix;
-              d = import ./file.nix { };
-              e = pythonPackages.callPackage ./file.nix { };
-              f = callPackage ./file.nix { };
-              g = callPackage ({ }: { }) { };
-              h = callPackage ./file.nix { x = 0; };
-              i = callPackage ({ }: { }) (let in { });
-            }
-        "#};
-
-        std::fs::write(&file, contents)?;
-
-        let nix_file = NixFile::new(&file)?;
-
-        let cases = [
-            (2, None),
-            (3, None),
-            (4, None),
-            (5, None),
-            (
-                6,
-                Some(CallPackageArgumentInfo {
-                    relative_path: Some(RelativePathBuf::from("file.nix")),
-                    empty_arg: true,
-                }),
-            ),
-            (
-                7,
-                Some(CallPackageArgumentInfo {
-                    relative_path: Some(RelativePathBuf::from("file.nix")),
-                    empty_arg: true,
-                }),
-            ),
-            (
-                8,
-                Some(CallPackageArgumentInfo {
-                    relative_path: None,
-                    empty_arg: true,
-                }),
-            ),
-            (
-                9,
-                Some(CallPackageArgumentInfo {
-                    relative_path: Some(RelativePathBuf::from("file.nix")),
-                    empty_arg: false,
-                }),
-            ),
-            (
-                10,
-                Some(CallPackageArgumentInfo {
-                    relative_path: None,
-                    empty_arg: false,
-                }),
-            ),
-        ];
-
-        for (line, expected_result) in cases {
-            let (actual_result, _definition) = nix_file
-                .call_package_argument_info_at(line, 3, temp_dir.path())
-                .context(format!("line {line}"))?;
-            assert_eq!(actual_result, expected_result, "line {line}");
-        }
-
-        Ok(())
-    }
-}