tests.nixpkgs-check-by-name: Better error for non-syntactic callPackage

This commit is contained in:
Silvan Mosberger 2024-02-19 23:35:18 +01:00
parent a61c8c9f5c
commit 24069b982d
5 changed files with 111 additions and 53 deletions

View file

@ -51,6 +51,26 @@ struct Location {
pub column: usize, pub column: usize,
} }
impl Location {
// Creates a [String] from a [Location], in a format like `{file}:{line}:{column}`,
// where `file` is relative to the given [Path].
fn to_relative_string(&self, nixpkgs_path: &Path) -> anyhow::Result<String> {
let relative = self.file.strip_prefix(nixpkgs_path).with_context(|| {
format!(
"An attributes location ({}) is outside Nixpkgs ({})",
self.file.display(),
nixpkgs_path.display()
)
})?;
Ok(format!(
"{}:{}:{}",
relative.display(),
self.line,
self.column
))
}
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub enum AttributeVariant { pub enum AttributeVariant {
/// The attribute is not an attribute set, we're limited in the amount of information we can get /// The attribute is not an attribute set, we're limited in the amount of information we can get
@ -289,37 +309,47 @@ fn by_name(
// At this point, we completed two different checks for whether it's a // At this point, we completed two different checks for whether it's a
// `callPackage` // `callPackage`
match (is_semantic_call_package, optional_syntactic_call_package) { match (is_semantic_call_package, optional_syntactic_call_package) {
// Something like `<attr> = { ... }` // Something like `<attr> = foo`
(false, None) (_, Err(definition)) => NixpkgsProblem::NonSyntacticCallPackage {
package_name: attribute_name.to_owned(),
location: location.to_relative_string(nixpkgs_path)?,
definition,
}
.into(),
// Something like `<attr> = pythonPackages.callPackage ...` // Something like `<attr> = pythonPackages.callPackage ...`
| (false, Some(_)) (false, Ok(_)) => {
// Something like `<attr> = bar` where `bar = pkgs.callPackage ...`
| (true, None) => {
// All of these are not of the expected form, so error out // All of these are not of the expected form, so error out
// TODO: Make error more specific, don't lump everything together // TODO: Make error more specific, don't lump everything together
NixpkgsProblem::WrongCallPackage { NixpkgsProblem::WrongCallPackage {
relative_package_file: relative_package_file.to_owned(), relative_package_file: relative_package_file.to_owned(),
package_name: attribute_name.to_owned(), package_name: attribute_name.to_owned(),
}.into() }
.into()
} }
// Something like `<attr> = pkgs.callPackage ...` // Something like `<attr> = pkgs.callPackage ...`
(true, Some(syntactic_call_package)) => { (true, Ok(syntactic_call_package)) => {
if let Some(path) = syntactic_call_package.relative_path { if let Some(path) = syntactic_call_package.relative_path {
if path == relative_package_file { if path == relative_package_file {
// Manual definitions with empty arguments are not allowed // Manual definitions with empty arguments are not allowed
// anymore // anymore
Success(if syntactic_call_package.empty_arg { Loose(()) } else { Tight }) Success(if syntactic_call_package.empty_arg {
Loose(())
} else {
Tight
})
} else { } else {
NixpkgsProblem::WrongCallPackage { NixpkgsProblem::WrongCallPackage {
relative_package_file: relative_package_file.to_owned(), relative_package_file: relative_package_file.to_owned(),
package_name: attribute_name.to_owned(), package_name: attribute_name.to_owned(),
}.into() }
.into()
} }
} else { } else {
NixpkgsProblem::WrongCallPackage { NixpkgsProblem::WrongCallPackage {
relative_package_file: relative_package_file.to_owned(), relative_package_file: relative_package_file.to_owned(),
package_name: attribute_name.to_owned(), package_name: attribute_name.to_owned(),
}.into() }
.into()
} }
} }
} }
@ -423,16 +453,16 @@ fn handle_non_by_name_attribute(
// `callPackage` // `callPackage`
match (is_semantic_call_package, optional_syntactic_call_package) { match (is_semantic_call_package, optional_syntactic_call_package) {
// Something like `<attr> = { }` // Something like `<attr> = { }`
(false, None) (false, Err(_))
// Something like `<attr> = pythonPackages.callPackage ...` // Something like `<attr> = pythonPackages.callPackage ...`
| (false, Some(_)) | (false, Ok(_))
// Something like `<attr> = bar` where `bar = pkgs.callPackage ...` // Something like `<attr> = bar` where `bar = pkgs.callPackage ...`
| (true, None) => { | (true, Err(_)) => {
// In all of these cases, it's not possible to migrate the package to `pkgs/by-name` // In all of these cases, it's not possible to migrate the package to `pkgs/by-name`
NonApplicable NonApplicable
} }
// Something like `<attr> = pkgs.callPackage ...` // Something like `<attr> = pkgs.callPackage ...`
(true, Some(syntactic_call_package)) => { (true, Ok(syntactic_call_package)) => {
// It's only possible to migrate such a definitions if.. // It's only possible to migrate such a definitions if..
match syntactic_call_package.relative_path { match syntactic_call_package.relative_path {
Some(ref rel_path) if rel_path.starts_with(utils::BASE_SUBPATH) => { Some(ref rel_path) if rel_path.starts_with(utils::BASE_SUBPATH) => {

View file

@ -86,8 +86,9 @@ pub struct CallPackageArgumentInfo {
impl NixFile { impl NixFile {
/// Returns information about callPackage arguments for an attribute at a specific line/column /// Returns information about callPackage arguments for an attribute at a specific line/column
/// index. /// index.
/// If the location is not of the form `<attr> = callPackage <arg1> <arg2>;`, `None` is /// If the location is not of the form `<attr> = callPackage <arg1> <arg2>;`, `Ok(Err(String))` is
/// returned. /// returned, with String being how the definition looks like.
///
/// This function only returns `Err` for problems that can't be caused by the Nix contents, /// This function only returns `Err` for problems that can't be caused by the Nix contents,
/// but rather problems in this programs code itself. /// but rather problems in this programs code itself.
/// ///
@ -108,7 +109,7 @@ impl NixFile {
/// ///
/// You'll get back /// You'll get back
/// ```rust /// ```rust
/// Some(CallPackageArgumentInfo { path = Some("default.nix"), empty_arg: true }) /// Ok(Ok(CallPackageArgumentInfo { path = Some("default.nix"), empty_arg: true }))
/// ``` /// ```
/// ///
/// Note that this also returns the same for `pythonPackages.callPackage`. It doesn't make an /// Note that this also returns the same for `pythonPackages.callPackage`. It doesn't make an
@ -118,11 +119,15 @@ impl NixFile {
line: usize, line: usize,
column: usize, column: usize,
relative_to: &Path, relative_to: &Path,
) -> anyhow::Result<Option<CallPackageArgumentInfo>> { ) -> anyhow::Result<Result<CallPackageArgumentInfo, String>> {
let Some(attrpath_value) = self.attrpath_value_at(line, column)? else { Ok(match self.attrpath_value_at(line, column)? {
return Ok(None); Err(definition) => Err(definition),
}; Ok(attrpath_value) => {
self.attrpath_value_call_package_argument_info(attrpath_value, relative_to) let definition = attrpath_value.to_string();
self.attrpath_value_call_package_argument_info(attrpath_value, relative_to)?
.ok_or(definition)
}
})
} }
// Internal function mainly to make it independently testable // Internal function mainly to make it independently testable
@ -130,7 +135,7 @@ impl NixFile {
&self, &self,
line: usize, line: usize,
column: usize, column: usize,
) -> anyhow::Result<Option<ast::AttrpathValue>> { ) -> anyhow::Result<Result<ast::AttrpathValue, String>> {
let index = self.line_index.fromlinecolumn(line, column); let index = self.line_index.fromlinecolumn(line, column);
let token_at_offset = self let token_at_offset = self
@ -164,7 +169,7 @@ impl NixFile {
// This is the only other way how `builtins.unsafeGetAttrPos` can return // This is the only other way how `builtins.unsafeGetAttrPos` can return
// attribute positions, but we only look for ones like `<attr-path> = <value>`, so // attribute positions, but we only look for ones like `<attr-path> = <value>`, so
// ignore this // ignore this
return Ok(None); return Ok(Err(node.to_string()));
} else { } else {
// However, anything else is not expected and smells like a bug // However, anything else is not expected and smells like a bug
anyhow::bail!( anyhow::bail!(
@ -208,7 +213,7 @@ impl NixFile {
// unwrap is fine because we confirmed that we can cast with the above check. // 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, // 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. // but we still need it for the error message when the cast fails.
Ok(Some(ast::AttrpathValue::cast(attrpath_value_node).unwrap())) Ok(Ok(ast::AttrpathValue::cast(attrpath_value_node).unwrap()))
} }
// Internal function mainly to make attrpath_value_at independently testable // Internal function mainly to make attrpath_value_at independently testable
@ -437,14 +442,14 @@ mod tests {
// These are builtins.unsafeGetAttrPos locations for the attributes // These are builtins.unsafeGetAttrPos locations for the attributes
let cases = [ let cases = [
(2, 3, Some("foo = 1;")), (2, 3, Ok("foo = 1;")),
(3, 3, Some(r#""bar" = 2;"#)), (3, 3, Ok(r#""bar" = 2;"#)),
(4, 3, Some(r#"${"baz"} = 3;"#)), (4, 3, Ok(r#"${"baz"} = 3;"#)),
(5, 3, Some(r#""${"qux"}" = 4;"#)), (5, 3, Ok(r#""${"qux"}" = 4;"#)),
(8, 3, Some("quux\n # B\n =\n # C\n 5\n # D\n ;")), (8, 3, Ok("quux\n # B\n =\n # C\n 5\n # D\n ;")),
(17, 7, Some("quuux/**/=/**/5/**/;")), (17, 7, Ok("quuux/**/=/**/5/**/;")),
(19, 10, None), (19, 10, Err("inherit toInherit;")),
(20, 22, None), (20, 22, Err("inherit (toInherit) toInherit;")),
]; ];
for (line, column, expected_result) in cases { for (line, column, expected_result) in cases {
@ -452,7 +457,13 @@ mod tests {
.attrpath_value_at(line, column) .attrpath_value_at(line, column)
.context(format!("line {line}, column {column}"))? .context(format!("line {line}, column {column}"))?
.map(|node| node.to_string()); .map(|node| node.to_string());
assert_eq!(actual_result.as_deref(), expected_result); let owned_expected_result = expected_result
.map(|x| x.to_string())
.map_err(|x| x.to_string());
assert_eq!(
actual_result, owned_expected_result,
"line {line}, column {column}"
);
} }
Ok(()) Ok(())
@ -481,41 +492,41 @@ mod tests {
let nix_file = NixFile::new(&file)?; let nix_file = NixFile::new(&file)?;
let cases = [ let cases = [
(2, None), (2, Err("a.sub = null;")),
(3, None), (3, Err("b = null;")),
(4, None), (4, Err("c = import ./file.nix;")),
(5, None), (5, Err("d = import ./file.nix { };")),
( (
6, 6,
Some(CallPackageArgumentInfo { Ok(CallPackageArgumentInfo {
relative_path: Some(PathBuf::from("file.nix")), relative_path: Some(PathBuf::from("file.nix")),
empty_arg: true, empty_arg: true,
}), }),
), ),
( (
7, 7,
Some(CallPackageArgumentInfo { Ok(CallPackageArgumentInfo {
relative_path: Some(PathBuf::from("file.nix")), relative_path: Some(PathBuf::from("file.nix")),
empty_arg: true, empty_arg: true,
}), }),
), ),
( (
8, 8,
Some(CallPackageArgumentInfo { Ok(CallPackageArgumentInfo {
relative_path: None, relative_path: None,
empty_arg: true, empty_arg: true,
}), }),
), ),
( (
9, 9,
Some(CallPackageArgumentInfo { Ok(CallPackageArgumentInfo {
relative_path: Some(PathBuf::from("file.nix")), relative_path: Some(PathBuf::from("file.nix")),
empty_arg: false, empty_arg: false,
}), }),
), ),
( (
10, 10,
Some(CallPackageArgumentInfo { Ok(CallPackageArgumentInfo {
relative_path: None, relative_path: None,
empty_arg: false, empty_arg: false,
}), }),
@ -525,8 +536,10 @@ mod tests {
for (line, expected_result) in cases { for (line, expected_result) in cases {
let actual_result = nix_file let actual_result = nix_file
.call_package_argument_info_at(line, 3, temp_dir.path()) .call_package_argument_info_at(line, 3, temp_dir.path())
.context(format!("line {line}"))?; .context(format!("line {line}"))?
assert_eq!(actual_result, expected_result); .map_err(|x| x);
let owned_expected_result = expected_result.map_err(|x| x.to_string());
assert_eq!(actual_result, owned_expected_result, "line {line}");
} }
Ok(()) Ok(())

View file

@ -44,6 +44,11 @@ pub enum NixpkgsProblem {
relative_package_file: PathBuf, relative_package_file: PathBuf,
package_name: String, package_name: String,
}, },
NonSyntacticCallPackage {
package_name: String,
location: String,
definition: String,
},
NonDerivation { NonDerivation {
relative_package_file: PathBuf, relative_package_file: PathBuf,
package_name: String, package_name: String,
@ -164,6 +169,14 @@ impl fmt::Display for NixpkgsProblem {
"pkgs.{package_name}: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage {} {{ ... }}` with a non-empty second argument.", "pkgs.{package_name}: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage {} {{ ... }}` with a non-empty second argument.",
relative_package_file.display() relative_package_file.display()
), ),
NixpkgsProblem::NonSyntacticCallPackage { package_name, location, definition } =>
write!(
f,
"Because {} exists, the attribute `pkgs.{package_name}` must be defined as `callPackage {} {{ ... }}`. This is however not the case: The attribute is defined in {location} as\n\t{}",
structure::relative_dir_for_package(package_name).display(),
structure::relative_file_for_package(package_name).display(),
definition,
),
NixpkgsProblem::NonDerivation { relative_package_file, package_name } => NixpkgsProblem::NonDerivation { relative_package_file, package_name } =>
write!( write!(
f, f,

View file

@ -1 +1,2 @@
pkgs.foo: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage pkgs/by-name/fo/foo/package.nix { ... }` with a non-empty second argument. Because pkgs/by-name/fo/foo exists, the attribute `pkgs.foo` must be defined as `callPackage pkgs/by-name/fo/foo/package.nix { ... }`. This is however not the case: The attribute is defined in all-packages.nix:4:3 as
foo = self.bar;

View file

@ -1 +1,2 @@
pkgs.nonDerivation: This attribute is manually defined (most likely in pkgs/top-level/all-packages.nix), which is only allowed if the definition is of the form `pkgs.callPackage pkgs/by-name/no/nonDerivation/package.nix { ... }` with a non-empty second argument. Because pkgs/by-name/no/nonDerivation exists, the attribute `pkgs.nonDerivation` must be defined as `callPackage pkgs/by-name/no/nonDerivation/package.nix { ... }`. This is however not the case: The attribute is defined in all-packages.nix:2:3 as
nonDerivation = self.someDrv;