provenant-cli 0.0.8

Provenant is a high-performance Rust scanner for licenses, packages, and source provenance.
Documentation
#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::TempDir;

    use crate::models::{DatasourceId, PackageType};
    use crate::parsers::{NixDefaultParser, NixFlakeLockParser, NixFlakeParser, PackageParser};

    fn create_named_manifest(
        dir_name: &str,
        file_name: &str,
        content: &str,
    ) -> (TempDir, std::path::PathBuf) {
        let temp_dir = TempDir::new().expect("Failed to create temp dir");
        let manifest_dir = temp_dir.path().join(dir_name);
        fs::create_dir_all(&manifest_dir).expect("Failed to create manifest dir");
        let manifest_path = manifest_dir.join(file_name);
        fs::write(&manifest_path, content).expect("Failed to write manifest");
        (temp_dir, manifest_path)
    }

    #[test]
    fn test_nix_parsers_match_expected_file_names() {
        assert!(NixFlakeParser::is_match(std::path::Path::new("flake.nix")));
        assert!(NixFlakeLockParser::is_match(std::path::Path::new(
            "flake.lock"
        )));
        assert!(!NixFlakeParser::is_match(std::path::Path::new(
            "flake.lock"
        )));
        assert!(NixDefaultParser::is_match(std::path::Path::new(
            "default.nix"
        )));
        assert!(!NixDefaultParser::is_match(std::path::Path::new(
            "shell.nix"
        )));
    }

    #[test]
    fn test_extract_flake_lock_dependency_graph() {
        let (_temp_dir, path) = create_named_manifest(
            "demo-lock",
            "flake.lock",
            r#"{
  "version": 7,
  "root": "root",
  "nodes": {
    "root": {
      "inputs": {
        "nixpkgs": "nixpkgs",
        "flake-utils": "flake-utils",
        "crate2nix": "crate2nix"
      }
    },
    "nixpkgs": {
      "locked": {
        "type": "github",
        "owner": "NixOS",
        "repo": "nixpkgs",
        "rev": "abc123",
        "narHash": "sha256-nixpkgs",
        "lastModified": 1710000000
      },
      "original": {
        "type": "indirect",
        "id": "nixpkgs"
      }
    },
    "flake-utils": {
      "locked": {
        "type": "github",
        "owner": "numtide",
        "repo": "flake-utils",
        "rev": "def456",
        "narHash": "sha256-flake-utils",
        "lastModified": 1710000001
      },
      "original": {
        "type": "github",
        "owner": "numtide",
        "repo": "flake-utils"
      }
    },
    "crate2nix": {
      "locked": {
        "type": "github",
        "owner": "nix-community",
        "repo": "crate2nix",
        "rev": "ghi789",
        "narHash": "sha256-crate2nix",
        "lastModified": 1710000002
      },
      "original": {
        "type": "github",
        "owner": "nix-community",
        "repo": "crate2nix"
      },
      "flake": false
    }
  }
}
"#,
        );

        let package = NixFlakeLockParser::extract_first_package(&path);

        assert_eq!(package.package_type, Some(PackageType::Nix));
        assert_eq!(package.datasource_id, Some(DatasourceId::NixFlakeLock));
        assert_eq!(package.primary_language.as_deref(), Some("JSON"));
        assert_eq!(package.name.as_deref(), Some("demo-lock"));
        assert_eq!(package.dependencies.len(), 3);
        assert_eq!(
            package
                .extra_data
                .as_ref()
                .and_then(|data| data.get("lock_version"))
                .and_then(|value| value.as_i64()),
            Some(7)
        );
        assert_eq!(
            package
                .extra_data
                .as_ref()
                .and_then(|data| data.get("root"))
                .and_then(|value| value.as_str()),
            Some("root")
        );

        let nixpkgs = package
            .dependencies
            .iter()
            .find(|dep| dep.purl.as_deref() == Some("pkg:nix/nixpkgs@abc123"))
            .expect("missing nixpkgs dependency");
        assert_eq!(nixpkgs.scope.as_deref(), Some("inputs"));
        assert_eq!(nixpkgs.is_pinned, Some(true));
        assert_eq!(
            nixpkgs
                .extra_data
                .as_ref()
                .and_then(|data| data.get("source_type"))
                .and_then(|value| value.as_str()),
            Some("github")
        );

        let crate2nix = package
            .dependencies
            .iter()
            .find(|dep| dep.purl.as_deref() == Some("pkg:nix/crate2nix@ghi789"))
            .expect("missing crate2nix dependency");
        assert_eq!(
            crate2nix
                .extra_data
                .as_ref()
                .and_then(|data| data.get("flake"))
                .and_then(|value| value.as_bool()),
            Some(false)
        );
    }

    #[test]
    fn test_extract_flake_metadata_and_inputs() {
        let (_temp_dir, path) = create_named_manifest(
            "demo-flake",
            "flake.nix",
            r#"{
  description = "Demo flake package";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
    flake-utils = {
      url = "github:numtide/flake-utils";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    rust-overlay.url = "github:oxalica/rust-overlay";
  };

  outputs = { self, nixpkgs, flake-utils, rust-overlay }: {};
}
"#,
        );

        let package = NixFlakeParser::extract_first_package(&path);

        assert_eq!(package.package_type, Some(PackageType::Nix));
        assert_eq!(package.datasource_id, Some(DatasourceId::NixFlakeNix));
        assert_eq!(package.primary_language.as_deref(), Some("Nix"));
        assert_eq!(package.name.as_deref(), Some("demo-flake"));
        assert_eq!(package.description.as_deref(), Some("Demo flake package"));
        assert_eq!(package.purl.as_deref(), Some("pkg:nix/demo-flake"));
        assert_eq!(package.dependencies.len(), 3);

        let nixpkgs = package
            .dependencies
            .iter()
            .find(|dep| dep.purl.as_deref() == Some("pkg:nix/nixpkgs"))
            .expect("missing nixpkgs dependency");
        assert_eq!(
            nixpkgs.extracted_requirement.as_deref(),
            Some("github:NixOS/nixpkgs/nixos-24.11")
        );
        assert_eq!(nixpkgs.scope.as_deref(), Some("inputs"));
        assert_eq!(nixpkgs.is_runtime, Some(false));
        assert_eq!(nixpkgs.is_optional, Some(false));
        assert_eq!(nixpkgs.is_direct, Some(true));

        let flake_utils = package
            .dependencies
            .iter()
            .find(|dep| dep.purl.as_deref() == Some("pkg:nix/flake-utils"))
            .expect("missing flake-utils dependency");
        assert_eq!(
            flake_utils.extracted_requirement.as_deref(),
            Some("github:numtide/flake-utils")
        );
        assert_eq!(
            flake_utils
                .extra_data
                .as_ref()
                .and_then(|data| data.get("follows"))
                .and_then(|value| value.as_str()),
            Some("nixpkgs")
        );
    }

    #[test]
    fn test_extract_default_nix_derivation_metadata_and_dependencies() {
        let (_temp_dir, path) = create_named_manifest(
            "demo-derivation",
            "default.nix",
            r#"{ lib, stdenv, fetchFromGitHub, pkg-config, openssl, zlib }:
stdenv.mkDerivation rec {
  pname = "demo";
  version = "1.2.3";
  homepage = "https://example.com/demo";

  nativeBuildInputs = [ pkg-config ];
  buildInputs = [ openssl zlib ];

  meta = with lib; {
    description = "Demo package";
    homepage = "https://example.com/demo";
    license = licenses.mit;
  };
}
"#,
        );

        let package = NixDefaultParser::extract_first_package(&path);

        assert_eq!(package.package_type, Some(PackageType::Nix));
        assert_eq!(package.datasource_id, Some(DatasourceId::NixDefaultNix));
        assert_eq!(package.primary_language.as_deref(), Some("Nix"));
        assert_eq!(package.name.as_deref(), Some("demo"));
        assert_eq!(package.version.as_deref(), Some("1.2.3"));
        assert_eq!(package.description.as_deref(), Some("Demo package"));
        assert_eq!(
            package.homepage_url.as_deref(),
            Some("https://example.com/demo")
        );
        assert_eq!(
            package.extracted_license_statement.as_deref(),
            Some("licenses.mit")
        );
        assert_eq!(package.purl.as_deref(), Some("pkg:nix/demo@1.2.3"));
        assert_eq!(package.dependencies.len(), 3);

        let native = package
            .dependencies
            .iter()
            .find(|dep| dep.purl.as_deref() == Some("pkg:nix/pkg-config"))
            .expect("missing native build input");
        assert_eq!(native.scope.as_deref(), Some("nativeBuildInputs"));
        assert_eq!(native.is_runtime, Some(false));

        let openssl = package
            .dependencies
            .iter()
            .find(|dep| dep.purl.as_deref() == Some("pkg:nix/openssl"))
            .expect("missing openssl input");
        assert_eq!(openssl.scope.as_deref(), Some("buildInputs"));
        assert_eq!(openssl.is_runtime, Some(true));
    }

    #[test]
    fn test_invalid_nix_inputs_preserve_datasource_identity() {
        let (_flake_dir, flake_path) =
            create_named_manifest("broken-flake", "flake.nix", "{ description = ; }");
        let (_lock_dir, lock_path) = create_named_manifest(
            "broken-lock",
            "flake.lock",
            "{ \"version\": 7, \"root\": \"root\" }",
        );
        let (_default_dir, default_path) =
            create_named_manifest("broken-default", "default.nix", "stdenv.mkDerivation {");

        let flake = NixFlakeParser::extract_first_package(&flake_path);
        let lock = NixFlakeLockParser::extract_first_package(&lock_path);
        let default_pkg = NixDefaultParser::extract_first_package(&default_path);

        assert_eq!(flake.package_type, Some(PackageType::Nix));
        assert_eq!(flake.datasource_id, Some(DatasourceId::NixFlakeNix));
        assert!(flake.name.is_none());
        assert!(flake.dependencies.is_empty());

        assert_eq!(lock.package_type, Some(PackageType::Nix));
        assert_eq!(lock.datasource_id, Some(DatasourceId::NixFlakeLock));
        assert!(lock.name.is_none());
        assert!(lock.dependencies.is_empty());

        assert_eq!(default_pkg.package_type, Some(PackageType::Nix));
        assert_eq!(default_pkg.datasource_id, Some(DatasourceId::NixDefaultNix));
        assert!(default_pkg.name.is_none());
        assert!(default_pkg.dependencies.is_empty());
    }

    #[test]
    fn test_default_nix_ignores_nested_non_root_mk_derivation() {
        let (_temp_dir, path) = create_named_manifest(
            "nested-helper",
            "default.nix",
            r#"{
  helper = stdenv.mkDerivation {
    pname = "nested";
    version = "1.0.0";
  };
}
"#,
        );

        let package = NixDefaultParser::extract_first_package(&path);

        assert_eq!(package.package_type, Some(PackageType::Nix));
        assert_eq!(package.datasource_id, Some(DatasourceId::NixDefaultNix));
        assert!(package.name.is_none());
        assert!(package.version.is_none());
        assert!(package.dependencies.is_empty());
    }
}