use std::collections::BTreeMap;
use anyhow::Context;
use serde::Deserialize;
use crate::{LintLevel, OverrideMap, QueryOverride, RequiredSemverUpdate};
#[derive(Debug, Clone)]
pub(crate) struct Manifest {
pub(crate) path: std::path::PathBuf,
pub(crate) parsed: cargo_toml::Manifest<MetadataTable>,
}
impl Manifest {
pub(crate) fn parse(path: std::path::PathBuf) -> anyhow::Result<Self> {
let parsed = cargo_toml::Manifest::from_path_with_metadata(&path)
.with_context(|| format!("failed when reading {}", path.display()))?;
Ok(Self { path, parsed })
}
pub(crate) fn parse_standalone(path: std::path::PathBuf) -> anyhow::Result<Self> {
let parsed = std::fs::read_to_string(&path)
.map_err(anyhow::Error::from)
.and_then(|data| {
cargo_toml::Manifest::from_slice_with_metadata(data.as_bytes())
.map_err(anyhow::Error::from)
})
.with_context(|| format!("failed when reading {}", path.display()))?;
Ok(Self { path, parsed })
}
}
pub(crate) fn get_package_name(manifest: &Manifest) -> anyhow::Result<&str> {
let package = manifest.parsed.package.as_ref().with_context(|| {
format!(
"failed to parse {}: no `package` table",
manifest.path.display()
)
})?;
Ok(&package.name)
}
pub(crate) fn get_package_version(manifest: &Manifest) -> anyhow::Result<&str> {
let package = manifest.parsed.package.as_ref().with_context(|| {
format!(
"failed to parse {}: no `package` table",
manifest.path.display()
)
})?;
let version = package.version.get().with_context(|| {
format!(
"failed to retrieve package version from {}",
manifest.path.display()
)
})?;
Ok(version)
}
pub(crate) fn get_library_target_name(manifest: &Manifest) -> anyhow::Result<String> {
if let Some(name) = manifest
.parsed
.lib
.as_ref()
.and_then(|lib| lib.name.as_ref())
{
return Ok(name.clone());
}
Ok(get_package_name(manifest)?.replace('-', "_"))
}
pub(crate) fn get_project_dir_from_manifest_path(
manifest_path: &std::path::Path,
) -> anyhow::Result<std::path::PathBuf> {
assert!(
manifest_path.ends_with("Cargo.toml"),
"path {} isn't pointing to a manifest",
manifest_path.display()
);
let dir_path = manifest_path
.parent()
.context("manifest path doesn't have a parent")?;
Ok(dir_path.to_path_buf())
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct MetadataTable {
#[serde(default, rename = "cargo-semver-checks")]
pub(crate) config: Option<SemverChecksTable>,
}
#[derive(Debug, Clone, Deserialize)]
#[non_exhaustive]
pub(crate) struct SemverChecksTable {
pub(crate) lints: Option<LintTable>,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct LintTable {
#[serde(default, deserialize_with = "deserialize_workspace_key")]
pub(crate) workspace: bool,
#[serde(flatten)]
pub(crate) inner: BTreeMap<String, OverrideConfig>,
}
impl LintTable {
pub(crate) fn into_stack(self) -> Vec<OverrideMap> {
let mut map = BTreeMap::<_, OverrideMap>::new();
for (id, config) in self.inner {
let (priority, overrides) = match config {
OverrideConfig::Shorthand(lint_level) => (
0,
QueryOverride {
lint_level: Some(lint_level),
required_update: None,
},
),
OverrideConfig::Both {
level,
required_update,
priority,
} => (
priority,
QueryOverride {
lint_level: Some(level),
required_update: Some(required_update),
},
),
OverrideConfig::LintLevel { level, priority } => (
priority,
QueryOverride {
lint_level: Some(level),
required_update: None,
},
),
OverrideConfig::RequiredUpdate {
required_update,
priority,
} => (
priority,
QueryOverride {
lint_level: None,
required_update: Some(required_update),
},
),
};
map.entry(priority).or_default().insert(id, overrides);
}
map.into_values().rev().collect()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub(crate) enum OverrideConfig {
#[serde(rename_all = "kebab-case")]
Both {
level: LintLevel,
required_update: RequiredSemverUpdate,
#[serde(default)]
priority: i64,
},
#[serde(rename_all = "kebab-case")]
LintLevel {
level: LintLevel,
#[serde(default)]
priority: i64,
},
#[serde(rename_all = "kebab-case")]
RequiredUpdate {
required_update: RequiredSemverUpdate,
#[serde(default)]
priority: i64,
},
Shorthand(LintLevel),
}
fn deserialize_workspace_key<'de, D>(de: D) -> Result<bool, D::Error>
where
D: serde::Deserializer<'de>,
{
let option = Option::<bool>::deserialize(de)?;
match option {
Some(true) => Ok(true),
None => Ok(false),
Some(false) => Err(serde::de::Error::custom(
"`lints.workspace = false` is not valid configuration.\n\
Either set `lints.workspace = true` or omit the key entirely.",
)),
}
}
pub(crate) fn deserialize_lint_table(
metadata: &serde_json::Value,
) -> anyhow::Result<Option<LintTable>> {
let table = Option::<MetadataTable>::deserialize(metadata)?;
Ok(table.and_then(|table| table.config.and_then(|config| config.lints)))
}
#[cfg(test)]
mod tests {
use super::{LintTable, MetadataTable};
use crate::{OverrideMap, QueryOverride};
#[test]
fn test_deserialize_config() {
use crate::LintLevel::*;
use crate::RequiredSemverUpdate::*;
let manifest = r#"[package]
name = "cargo-semver-checks"
version = "1.2.3"
edition = "2021"
[package.metadata.cargo-semver-checks.lints]
workspace = true
two = "deny"
three = { level = "warn", priority = 1 }
four = { required-update = "major", priority = 0 }
five = { required-update = "minor", level = "allow", priority = -1 }
[workspace.metadata.cargo-semver-checks.lints]
six = "allow"
seven = { level = "deny", priority = 2 }
"#;
let parsed = cargo_toml::Manifest::from_slice_with_metadata(manifest.as_bytes())
.expect("Cargo.toml should be valid");
let package_metadata: MetadataTable = parsed
.package
.expect("Cargo.toml should contain a package")
.metadata
.expect("Package metadata should be present");
let workspace_metadata = parsed
.workspace
.expect("Cargo.toml should contain a workspace")
.metadata
.expect("Workspace metadata should be present");
let pkg_table = package_metadata
.config
.expect("Semver checks table should be present")
.lints
.expect("Lint table should be present");
assert!(
pkg_table.workspace,
"Package lints table should contain `workspace = true`"
);
let pkg = pkg_table.into_stack();
let wks = workspace_metadata
.config
.expect("Semver checks table should be present")
.lints
.expect("Lint table should be present")
.into_stack();
similar_asserts::assert_eq!(
wks,
vec![
OverrideMap::from_iter([(
"seven".into(),
QueryOverride {
lint_level: Some(Deny),
required_update: None,
}
),]),
OverrideMap::from_iter([(
"six".into(),
QueryOverride {
lint_level: Some(Allow),
required_update: None,
}
),]),
]
);
similar_asserts::assert_eq!(
pkg,
vec![
OverrideMap::from_iter([(
"three".into(),
QueryOverride {
lint_level: Some(Warn),
required_update: None,
}
)]),
OverrideMap::from_iter([
(
"two".into(),
QueryOverride {
lint_level: Some(Deny),
required_update: None
}
),
(
"four".into(),
QueryOverride {
lint_level: None,
required_update: Some(Major),
}
),
]),
OverrideMap::from_iter([(
"five".into(),
QueryOverride {
lint_level: Some(Allow),
required_update: Some(Minor),
}
)])
]
);
}
#[test]
fn workspace_key_false_is_error() {
serde_json::from_value::<LintTable>(serde_json::json! {{
"workspace": false
}})
.expect_err("`workspace = false` should not be accepted");
}
#[test]
fn workspace_key_omitted_is_false() {
let table = serde_json::from_value::<LintTable>(serde_json::json! {{
}})
.expect("this should be a valid lint table");
assert!(!table.workspace, "table.workspace should be false");
}
#[test]
fn entry_with_no_fields_is_error() {
toml::from_str::<LintTable>("one = {}").expect_err("one = {} should be invalid");
toml::from_str::<LintTable>("one = { priority = 0 }")
.expect_err("one = {priority = 0} should be invalid");
}
}