use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::error::ConfigError;
const SURFACE_LINTS: &[&str] = &[
"dead-public",
"unnecessary-public",
"unnecessary-restricted-visibility",
"unnecessary-crate-visibility",
];
const SURFACE_ITEM_KINDS: &[&str] = &[
"associated-constant",
"associated-type",
"constant",
"enum",
"field",
"foreign-function",
"foreign-static",
"function",
"impl",
"macro",
"method",
"module",
"reexport",
"static",
"struct",
"trait",
"type-alias",
"union",
"variant",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SurfaceConsumerScope {
#[default]
Open,
Workspace,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SurfaceCrateVisibility {
#[default]
Preserve,
Allow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SurfaceLintLevel {
#[default]
Allow,
Warn,
Deny,
Expect,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SurfaceLintDirective {
pub selector: String,
pub level: SurfaceLintLevel,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SurfaceFeatureProfile {
pub name: String,
#[serde(default, rename = "all-features")]
pub all_features: bool,
#[serde(default, rename = "no-default-features")]
pub no_default_features: bool,
#[serde(default)]
pub features: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SurfaceDoctest {
pub package: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum SurfaceDoctestCoverage {
#[default]
Automatic,
Disabled,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SurfaceExternal {
#[serde(rename = "crate")]
pub crate_name: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SurfaceProduct {
pub package: String,
pub bin: Option<String>,
pub lib: Option<String>,
pub target: Option<String>,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SurfaceOverride {
pub lint: String,
#[serde(default)]
pub package: Option<String>,
#[serde(default, rename = "crate")]
pub crate_name: Option<String>,
pub item: String,
pub kind: Option<String>,
pub target: Option<String>,
#[serde(default)]
pub level: SurfaceLintLevel,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SurfaceExclude {
#[serde(default)]
pub package: Option<String>,
#[serde(default, rename = "crate")]
pub crate_name: Option<String>,
pub module: Option<String>,
pub file: Option<String>,
pub target: Option<String>,
#[serde(default)]
pub level: SurfaceLintLevel,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct SurfaceConfig {
pub enabled: bool,
pub consumer_scope: SurfaceConsumerScope,
pub targets: Vec<String>,
pub crate_visibility: SurfaceCrateVisibility,
pub preserve_uniform_fields: bool,
pub lint: Vec<SurfaceLintDirective>,
pub product: Vec<SurfaceProduct>,
#[serde(rename = "feature-profile")]
pub feature_profile: Vec<SurfaceFeatureProfile>,
pub doctest: Vec<SurfaceDoctest>,
pub doctest_coverage: SurfaceDoctestCoverage,
pub external: Vec<SurfaceExternal>,
pub r#override: Vec<SurfaceOverride>,
pub exclude: Vec<SurfaceExclude>,
}
impl Default for SurfaceConfig {
fn default() -> Self {
Self {
enabled: false,
consumer_scope: SurfaceConsumerScope::Open,
targets: vec!["host".to_string()],
crate_visibility: SurfaceCrateVisibility::Preserve,
preserve_uniform_fields: false,
lint: Vec::new(),
product: Vec::new(),
feature_profile: Vec::new(),
doctest: Vec::new(),
doctest_coverage: SurfaceDoctestCoverage::Automatic,
external: Vec::new(),
r#override: Vec::new(),
exclude: Vec::new(),
}
}
}
impl SurfaceConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
if self.targets.is_empty() {
return Err(invalid("surface.targets", "must contain at least one target view"));
}
validate_unique_non_empty(&self.targets, "surface.targets")?;
for (index, directive) in self.lint.iter().enumerate() {
let field = format!("surface.lint.{index}");
if directive.selector != "warnings" {
validate_lint(&directive.selector, &format!("{field}.selector"))?;
}
if directive.level == SurfaceLintLevel::Expect {
return Err(invalid(field, "ordered lint policy cannot use 'expect'"));
}
}
let mut products = BTreeSet::new();
for (index, product) in self.product.iter().enumerate() {
let field = format!("surface.product.{index}");
validate_text(&product.package, &format!("{field}.package"))?;
validate_text(&product.reason, &format!("{field}.reason"))?;
validate_target_selector(product.target.as_deref(), &format!("{field}.target"))?;
let target = match (&product.bin, &product.lib) {
(Some(bin), None) => ("bin", bin),
(None, Some(lib)) => ("lib", lib),
_ => return Err(invalid(field, "requires exactly one of 'bin' or 'lib'")),
};
validate_text(target.1, &format!("{field}.{}", target.0))?;
if !products.insert((&product.package, target.0, target.1, &product.target)) {
return Err(invalid(field, "duplicates an earlier product root"));
}
}
let mut profile_names = BTreeSet::new();
for (index, profile) in self.feature_profile.iter().enumerate() {
let field = format!("surface.feature-profile.{index}");
validate_text(&profile.name, &format!("{field}.name"))?;
if !profile
.name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
{
return Err(invalid(
format!("{field}.name"),
"must use only ASCII letters, digits, '-' or '_'",
));
}
if !profile_names.insert(&profile.name) {
return Err(invalid(field, "duplicates an earlier feature profile name"));
}
if profile.all_features && (profile.no_default_features || !profile.features.is_empty()) {
return Err(invalid(
field,
"cannot combine 'all-features' with 'no-default-features' or explicit features",
));
}
validate_unique_non_empty(&profile.features, &format!("{field}.features"))?;
}
let doctests = self
.doctest
.iter()
.map(|entry| entry.package.clone())
.collect::<Vec<_>>();
validate_unique_non_empty(&doctests, "surface.doctest.package")?;
if self.doctest_coverage == SurfaceDoctestCoverage::Disabled && !self.doctest.is_empty() {
return Err(invalid(
"surface.doctest_coverage",
"cannot be 'disabled' when explicit doctest packages are configured",
));
}
let mut external = BTreeSet::new();
for (index, boundary) in self.external.iter().enumerate() {
let field = format!("surface.external.{index}");
validate_text(&boundary.crate_name, &format!("{field}.crate"))?;
validate_text(&boundary.reason, &format!("{field}.reason"))?;
if !external.insert(&boundary.crate_name) {
return Err(invalid(field, "duplicates an earlier external crate boundary"));
}
}
let mut overrides = BTreeSet::new();
for (index, policy) in self.r#override.iter().enumerate() {
let field = format!("surface.override.{index}");
validate_lint(&policy.lint, &format!("{field}.lint"))?;
validate_policy_owner(policy.package.as_deref(), policy.crate_name.as_deref(), &field)?;
validate_text(&policy.item, &format!("{field}.item"))?;
if policy
.kind
.as_deref()
.is_some_and(|kind| !SURFACE_ITEM_KINDS.contains(&kind))
{
return Err(invalid(
format!("{field}.kind"),
format!(
"unknown declaration kind '{}'",
policy.kind.as_deref().unwrap_or_default()
),
));
}
validate_target_selector(policy.target.as_deref(), &format!("{field}.target"))?;
validate_text(&policy.reason, &format!("{field}.reason"))?;
if !overrides.insert((
&policy.lint,
&policy.package,
&policy.crate_name,
&policy.item,
&policy.kind,
&policy.target,
)) {
return Err(invalid(field, "duplicates an earlier item override"));
}
}
let mut exclusions = BTreeSet::new();
for (index, policy) in self.exclude.iter().enumerate() {
let field = format!("surface.exclude.{index}");
validate_policy_owner(policy.package.as_deref(), policy.crate_name.as_deref(), &field)?;
validate_text(&policy.reason, &format!("{field}.reason"))?;
let selector = match (&policy.module, &policy.file) {
(Some(module), None) => ("module", module),
(None, Some(file)) => ("file", file),
_ => return Err(invalid(field, "requires exactly one of 'module' or 'file'")),
};
validate_text(selector.1, &format!("{field}.{}", selector.0))?;
validate_target_selector(policy.target.as_deref(), &format!("{field}.target"))?;
if !exclusions.insert((
&policy.package,
&policy.crate_name,
selector.0,
selector.1,
&policy.target,
)) {
return Err(invalid(field, "duplicates an earlier exclusion"));
}
}
Ok(())
}
}
fn validate_policy_owner(package: Option<&str>, crate_name: Option<&str>, field: &str) -> Result<(), ConfigError> {
match (package, crate_name) {
(Some(package), None) => validate_text(package, &format!("{field}.package")),
(None, Some(crate_name)) => validate_text(crate_name, &format!("{field}.crate")),
_ => Err(invalid(field, "requires exactly one of 'package' or 'crate'")),
}
}
fn validate_target_selector(target: Option<&str>, field: &str) -> Result<(), ConfigError> {
let Some(target) = target else {
return Ok(());
};
validate_text(target, field)?;
target
.parse::<cargo_platform::Platform>()
.map(|_| ())
.map_err(|error| invalid(field, format!("invalid Cargo target selector '{target}': {error}")))
}
fn validate_lint(value: &str, field: &str) -> Result<(), ConfigError> {
if SURFACE_LINTS.contains(&value) {
Ok(())
} else {
Err(invalid(field, format!("unknown surface lint '{value}'")))
}
}
fn validate_unique_non_empty(values: &[String], field: &str) -> Result<(), ConfigError> {
let mut unique = BTreeSet::new();
for value in values {
validate_text(value, field)?;
if !unique.insert(value) {
return Err(invalid(field, format!("contains duplicate value '{value}'")));
}
}
Ok(())
}
fn validate_text(value: &str, field: &str) -> Result<(), ConfigError> {
if value.trim().is_empty() || value != value.trim() {
Err(invalid(field, "must be non-empty and have no surrounding whitespace"))
} else {
Ok(())
}
}
fn invalid(field: impl Into<String>, reason: impl Into<String>) -> ConfigError {
ConfigError::InvalidField {
field: field.into(),
reason: reason.into(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn planner_enforcement_requires_explicit_enablement() {
assert!(!SurfaceConfig::default().enabled);
let config: SurfaceConfig = toml_edit::de::from_str("enabled = true").unwrap();
assert!(config.enabled);
}
#[test]
fn product_and_filter_selectors_are_exact() {
let mut config = SurfaceConfig::default();
config.product.push(SurfaceProduct {
package: "app".to_string(),
bin: Some("app".to_string()),
lib: None,
target: None,
reason: "shipped application".to_string(),
});
config.r#override.push(SurfaceOverride {
lint: "unnecessary-public".to_string(),
package: Some("app".to_string()),
crate_name: None,
item: "migration::legacy".to_string(),
kind: Some("function".to_string()),
target: None,
level: SurfaceLintLevel::Expect,
reason: "migration compatibility".to_string(),
});
config.exclude.push(SurfaceExclude {
package: Some("app".to_string()),
crate_name: None,
module: Some("generated".to_string()),
file: None,
target: None,
level: SurfaceLintLevel::Expect,
reason: "generated source".to_string(),
});
config.validate().unwrap();
config.product[0].lib = Some("app".to_string());
assert!(config.validate().is_err());
}
#[test]
fn unknown_lints_and_empty_reasons_are_rejected() {
let mut config = SurfaceConfig::default();
config.r#override.push(SurfaceOverride {
lint: "dead-code".to_string(),
package: Some("app".to_string()),
crate_name: None,
item: "legacy".to_string(),
kind: Some("function".to_string()),
target: None,
level: SurfaceLintLevel::Allow,
reason: "".to_string(),
});
assert!(config.validate().is_err());
}
}