use std::path::{Path, PathBuf};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use skif_core::{FileSystem, OsFs};
use skif_specifier::CONFIG_IDENTIFIERS;
use crate::dependency_types::{DependencyType, DependencyTypes};
use crate::error::ConfigError;
use crate::overrides::ConfigOverrides;
use crate::rules::{Level, RuleConfig, rules_map};
use crate::selector::{Selector, anchor_patterns};
#[cfg(feature = "schema")]
use crate::schema::RuleConfigSchema;
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Config {
#[serde(skip_serializing_if = "is_false")]
pub root: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub extends: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub reset: Vec<ResetKey>,
pub workspace: Workspace,
pub lint: Lint,
#[serde(with = "rules_map")]
#[cfg_attr(feature = "schema", schemars(with = "IndexMap<String, RuleConfigSchema>"))]
pub rules: IndexMap<String, RuleConfig>,
pub ignore: Vec<IgnoreEntry>,
pub policy: Policy,
pub version_groups: Vec<VersionGroup>,
pub semver_groups: Vec<SemverGroup>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub custom_types: Vec<CustomType>,
pub catalog: Catalog,
pub workspace_protocol: WorkspaceProtocol,
pub dependency_families: Vec<DependencyFamily>,
pub update: Update,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub update_groups: Vec<UpdateGroup>,
pub js: Js,
pub format: Format,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub overrides: Vec<Override>,
#[serde(skip)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub cli_levels: IndexMap<String, Level>,
#[serde(skip)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub filter: InstanceFilter,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct InstanceFilter {
pub dependencies: Selector,
pub dependency_types: DependencyTypes,
pub specifier_types: Vec<String>,
pub sources: Selector,
}
impl InstanceFilter {
pub fn new(
dependencies: &[String],
dependency_types: &[String],
specifier_types: &[String],
sources: &[String],
) -> Result<Self, ConfigError> {
if let Some(message) = unknown_specifier_type(specifier_types) {
return Err(ConfigError::invalid(None, format!("--specifier-types: {message}")));
}
Ok(Self {
dependencies: Selector::new(dependencies.iter().cloned())?,
dependency_types: DependencyTypes::new(dependency_types)?,
specifier_types: specifier_types.to_vec(),
sources: Selector::new(sources.iter().cloned())?,
})
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.dependencies.is_empty()
&& self.dependency_types.is_empty()
&& self.specifier_types.is_empty()
&& self.sources.is_empty()
}
#[must_use]
pub fn unknown_custom_type<'a>(&'a self, config: &Config) -> Option<&'a str> {
self.dependency_types
.custom_names()
.find(|name| config.custom_types.iter().all(|custom| custom.name != *name))
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ResetKey {
VersionGroups,
SemverGroups,
Ignore,
DependencyFamilies,
CustomTypes,
Overrides,
UpdateGroups,
}
impl ResetKey {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::VersionGroups => "version-groups",
Self::SemverGroups => "semver-groups",
Self::Ignore => "ignore",
Self::DependencyFamilies => "dependency-families",
Self::CustomTypes => "custom-types",
Self::Overrides => "overrides",
Self::UpdateGroups => "update-groups",
}
}
}
fn selector_problem(patterns: &[String]) -> Option<String> {
match Selector::new(patterns.iter().cloned()) {
Err(ConfigError::Invalid { message, .. }) => Some(message),
_ => None,
}
}
impl Config {
pub fn from_toml_str(source: &str) -> Result<Self, ConfigError> {
Self::parse(source, None)
}
pub fn from_toml_file(path: &Path) -> Result<Self, ConfigError> {
Self::from_toml_file_with_fs(path, &OsFs)
}
pub fn from_toml_file_with_fs(path: &Path, fs: &dyn FileSystem) -> Result<Self, ConfigError> {
let source = fs.read_to_string(path).map_err(|source| ConfigError::Io {
path: path.to_path_buf(),
source,
})?;
Self::parse(&source, Some(path))
}
pub(crate) fn from_document(
document: toml::Spanned<toml::de::DeTable<'_>>,
source: &str,
path: Option<&Path>,
resolve_references: bool,
) -> Result<Self, ConfigError> {
let config = Self::deserialize(toml::de::Deserializer::from(document))
.map_err(|err| ConfigError::from_toml(&err, source, path))?;
config.validate_with(path, resolve_references)?;
Ok(config)
}
fn parse(source: &str, path: Option<&Path>) -> Result<Self, ConfigError> {
let from_toml = |err: &toml::de::Error| ConfigError::from_toml(err, source, path);
let document = toml::de::DeTable::parse(source).map_err(|err| from_toml(&err))?;
let config = Self::from_document(document.clone(), source, path, true)?;
if !config.has_extend_keys() {
return Ok(config);
}
let table = toml::Table::deserialize(toml::de::Deserializer::from(document)).map_err(|err| from_toml(&err))?;
let mut merger = crate::merge::Merger::default();
merger.apply(0, &table, &std::collections::HashMap::new());
let folded = merger.into_table();
toml::Value::Table(folded)
.try_into()
.map_err(|err: toml::de::Error| ConfigError::invalid(path, err.message()))
}
fn has_extend_keys(&self) -> bool {
!self.workspace.extend_ignore_packages.is_empty()
|| !self.js.extend_ignore.is_empty()
|| !self.js.extend_ignore_dependencies.is_empty()
|| !self.js.extend_ignore_binaries.is_empty()
|| !self.js.extend_ignore_unresolved.is_empty()
|| !self.catalog.extend_exclude.is_empty()
}
pub fn to_toml_string(&self) -> Result<String, toml::ser::Error> {
toml::to_string(self)
}
pub fn validate(&self) -> Result<(), ConfigError> {
self.validate_with(None, true)
}
fn validate_with(&self, path: Option<&Path>, resolve_references: bool) -> Result<(), ConfigError> {
let invalid = |message: String| Err(ConfigError::invalid(path, message));
for (index, group) in self.version_groups.iter().enumerate() {
if let Some(message) = group.inconsistency() {
return invalid(format!("version-groups[{index}]: {message}"));
}
if let Some(message) = unknown_specifier_type(&group.specifier_types) {
return invalid(format!("version-groups[{index}]: {message}"));
}
}
for (index, group) in self.semver_groups.iter().enumerate() {
if let Some(message) = group.inconsistency() {
return invalid(format!("semver-groups[{index}]: {message}"));
}
if let Some(message) = unknown_specifier_type(&group.specifier_types) {
return invalid(format!("semver-groups[{index}]: {message}"));
}
}
for (index, group) in self.update_groups.iter().enumerate() {
if let Some(message) = group.inconsistency() {
return invalid(format!("update-groups[{index}]: {message}"));
}
if let Some(message) = unknown_specifier_type(&group.specifier_types) {
return invalid(format!("update-groups[{index}]: {message}"));
}
}
if self.update.max_concurrent_requests == 0 {
return invalid("update.max-concurrent-requests must be at least 1".to_string());
}
if let Some(message) = self.custom_type_problem(resolve_references) {
return invalid(message);
}
for (index, entry) in self.ignore.iter().enumerate() {
if entry.rules.is_empty() && entry.packages.is_empty() && entry.dependencies.is_empty() {
return invalid(format!(
"ignore[{index}]: set at least one of `rules`, `packages` or `dependencies`; an entry without filters would suppress every diagnostic"
));
}
if entry.reason.as_deref().is_some_and(|reason| reason.trim().is_empty()) {
return invalid(format!("ignore[{index}]: `reason` is empty; say why or remove it"));
}
}
if let Some(message) = self.format.inconsistency() {
return invalid(format!("format.{message}"));
}
for (index, scoped) in self.overrides.iter().enumerate() {
if let Some(message) = scoped.inconsistency() {
return invalid(format!("overrides[{index}]: {message}"));
}
}
if self.catalog.min_packages == 0 {
return invalid("catalog.min-packages must be at least 1".to_string());
}
if let Some(message) = selector_problem(&self.catalog.exclude) {
return invalid(format!("catalog.exclude: {message}"));
}
for (index, pattern) in self.workspace.discover_ignore.iter().enumerate() {
let glob = pattern.strip_prefix('!').unwrap_or(pattern).trim_start_matches('/');
let mut builder = ignore::gitignore::GitignoreBuilder::new("");
let checked = builder
.add_line(None, pattern)
.map(|_| ())
.map_err(|err| err.to_string())
.and_then(|()| globset::Glob::new(glob).map(|_| ()).map_err(|err| err.to_string()));
if let Err(err) = checked {
return invalid(format!("workspace.discover-ignore[{index}]: {err}"));
}
}
for (index, family) in self.dependency_families.iter().enumerate() {
if family.name.is_empty() {
return invalid(format!("dependency-families[{index}]: `name` must not be empty"));
}
if family.enabled && family.packages.is_empty() {
return invalid(format!("dependency-families[{index}]: `packages` must not be empty"));
}
if let Some(message) = selector_problem(&family.packages) {
return invalid(format!("dependency-families[{index}]: {message}"));
}
}
Ok(())
}
fn custom_type_problem(&self, resolve_references: bool) -> Option<String> {
for (index, custom) in self.custom_types.iter().enumerate() {
if let Some(message) = custom.inconsistency() {
return Some(format!("custom-types[{index}]: {message}"));
}
if self.custom_types[..index]
.iter()
.any(|earlier| earlier.name == custom.name)
{
return Some(format!(
"custom-types[{index}]: the name \"{}\" is defined twice",
custom.name
));
}
}
if !resolve_references {
return None;
}
let lists = std::iter::once(("policy.dependency-types".to_owned(), &self.policy.dependency_types))
.chain(self.version_groups.iter().enumerate().map(|(index, group)| {
(
format!("version-groups[{index}].dependency-types"),
&group.dependency_types,
)
}))
.chain(self.semver_groups.iter().enumerate().map(|(index, group)| {
(
format!("semver-groups[{index}].dependency-types"),
&group.dependency_types,
)
}))
.chain(self.update_groups.iter().enumerate().map(|(index, group)| {
(
format!("update-groups[{index}].dependency-types"),
&group.dependency_types,
)
}));
for (place, types) in lists {
if let Some(name) = types
.custom_names()
.find(|name| self.custom_types.iter().all(|custom| custom.name != *name))
{
let hint = closest(
name,
&self
.custom_types
.iter()
.map(|custom| custom.name.as_str())
.chain(BUILTIN_TYPE_TOKENS)
.collect::<Vec<_>>(),
)
.map_or_else(String::new, |s| format!("; did you mean \"{s}\"?"));
return Some(format!(
"{place}: unknown dependency type \"{name}\"{hint} (expected {}, or the name of a `[[custom-types]]` entry)",
BUILTIN_TYPE_TOKENS.join(", ")
));
}
}
None
}
pub fn validate_rules(&self, known: &[&str]) -> Result<(), ConfigError> {
let configured = self.rules.keys();
let ignored = self.ignore.iter().flat_map(|entry| entry.rules.iter());
let scoped = self.overrides.iter().flat_map(|scoped| {
scoped
.rules
.keys()
.chain(scoped.ignore.iter().flat_map(|entry| entry.rules.iter()))
});
for id in configured.chain(ignored).chain(scoped) {
if !known.contains(&id.as_str()) {
return Err(ConfigError::UnknownRule {
rule: id.clone(),
suggestion: closest(id, known),
});
}
}
Ok(())
}
pub fn validate_plugins(&self, known: &[&str]) -> Result<(), ConfigError> {
let workspaces = self
.js
.workspaces
.iter()
.map(|(dir, workspace)| (format!("js.workspaces.\"{dir}\".plugins"), &workspace.plugins));
let overrides = self
.overrides
.iter()
.enumerate()
.map(|(index, scoped)| (format!("overrides[{index}].js.plugins"), &scoped.js.plugins));
let tables = std::iter::once(("js.plugins".to_owned(), &self.js.plugins))
.chain(workspaces)
.chain(overrides);
for (key, table) in tables {
if let Some(id) = table.keys().find(|id| !known.contains(&id.as_str())) {
let hint = closest(id, known).map_or_else(String::new, |close| format!("; did you mean `{close}`?"));
return Err(ConfigError::invalid(
None,
format!("{key}.{id}: unknown plugin `{id}`{hint}"),
));
}
}
Ok(())
}
#[must_use]
pub fn rule(&self, id: &str) -> Option<&RuleConfig> {
self.rules.get(id)
}
pub fn apply(&mut self, overrides: &ConfigOverrides) -> Result<(), ConfigError> {
let ignore_packages = Selector::new(overrides.ignore_packages.iter().cloned())?;
let ignore_dependencies = Selector::new(overrides.ignore_dependencies.iter().cloned())?;
for (id, level) in &overrides.rules {
self.rules.entry(id.clone()).or_default().severity = Some(*level);
self.cli_levels.insert(id.clone(), *level);
}
for id in &overrides.ignore_rules {
self.rules.entry(id.clone()).or_default().severity = Some(Level::Off);
self.cli_levels.insert(id.clone(), Level::Off);
}
if !ignore_packages.is_empty() {
self.workspace.ignore_packages = ignore_packages;
}
if !ignore_dependencies.is_empty() {
self.ignore.push(IgnoreEntry {
dependencies: ignore_dependencies,
reason: Some("--ignore-dependency".to_owned()),
..IgnoreEntry::default()
});
}
if let Some(value) = overrides.fail_on_warnings {
self.lint.fail_on_warnings = value;
}
if let Some(value) = overrides.production {
self.js.production = value;
}
if overrides.no_js {
self.js.enabled = false;
}
Ok(())
}
}
const BUILTIN_TYPE_TOKENS: [&str; 8] = [
"prod",
"dev",
"peer",
"optional",
"local",
"resolutions",
"overrides",
"pnpm-overrides",
];
fn unknown_specifier_type(specifier_types: &[String]) -> Option<String> {
specifier_types.iter().find_map(|raw| {
let name = raw.strip_prefix('!').unwrap_or(raw);
if CONFIG_IDENTIFIERS.contains(&name) {
return None;
}
let hint = closest(name, &CONFIG_IDENTIFIERS).map_or_else(String::new, |s| format!("; did you mean \"{s}\"?"));
Some(format!(
"unknown specifier type \"{name}\"{hint} (expected one of: {})",
CONFIG_IDENTIFIERS.join(", ")
))
})
}
fn closest(id: &str, known: &[&str]) -> Option<String> {
let limit = (id.chars().count() / 3).clamp(2, 4);
known
.iter()
.map(|candidate| (strsim::levenshtein(id, candidate), candidate))
.filter(|(distance, _)| *distance <= limit)
.min_by_key(|(distance, _)| *distance)
.map(|(_, candidate)| (*candidate).to_string())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum PackageManager {
Npm,
Pnpm,
Yarn,
Bun,
}
pub const FIXTURE_DIRECTORIES: [&str; 2] = ["__fixtures__", "fixtures"];
pub const DEFAULT_DISCOVER_IGNORE: [&str; 4] = ["__fixtures__", "fixtures", "__mocks__", "__snapshots__"];
fn default_discover_ignore() -> Vec<String> {
DEFAULT_DISCOVER_IGNORE.iter().map(|name| (*name).to_owned()).collect()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Workspace {
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub ignore_packages: Selector,
#[serde(skip_serializing_if = "Option::is_none")]
pub package_manager: Option<PackageManager>,
pub discover_ignore: Vec<String>,
#[serde(skip_serializing_if = "Selector::is_empty")]
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub extend_ignore_packages: Selector,
}
impl Default for Workspace {
fn default() -> Self {
Self {
ignore_packages: Selector::default(),
extend_ignore_packages: Selector::default(),
package_manager: None,
discover_ignore: default_discover_ignore(),
}
}
}
impl Workspace {
#[must_use]
pub fn with_ignore_packages(mut self, ignore_packages: Selector) -> Self {
self.ignore_packages = ignore_packages;
self
}
#[must_use]
pub const fn with_package_manager(mut self, package_manager: PackageManager) -> Self {
self.package_manager = Some(package_manager);
self
}
#[must_use]
pub fn with_discover_ignore(mut self, discover_ignore: Vec<String>) -> Self {
self.discover_ignore = discover_ignore;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Lint {
pub fail_on_warnings: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_level: Option<Level>,
}
impl Lint {
#[must_use]
pub const fn with_fail_on_warnings(mut self, fail_on_warnings: bool) -> Self {
self.fail_on_warnings = fail_on_warnings;
self
}
#[must_use]
pub const fn with_max_level(mut self, max_level: Level) -> Self {
self.max_level = Some(max_level);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct IgnoreEntry {
pub rules: Vec<String>,
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub packages: Selector,
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub dependencies: Selector,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
impl IgnoreEntry {
#[must_use]
pub fn with_rules<I, S>(mut self, rules: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.rules = rules.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_packages(mut self, packages: Selector) -> Self {
self.packages = packages;
self
}
#[must_use]
pub fn with_dependencies(mut self, dependencies: Selector) -> Self {
self.dependencies = dependencies;
self
}
#[must_use]
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum Prefer {
#[default]
Highest,
Lowest,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum OverrideReading {
#[default]
Skif,
Syncpack,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Policy {
pub prefer: Prefer,
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub dependency_types: DependencyTypes,
pub override_reading: OverrideReading,
}
impl Policy {
#[must_use]
pub const fn with_prefer(mut self, prefer: Prefer) -> Self {
self.prefer = prefer;
self
}
#[must_use]
pub fn with_dependency_types(mut self, dependency_types: DependencyTypes) -> Self {
self.dependency_types = dependency_types;
self
}
#[must_use]
pub const fn with_override_reading(mut self, override_reading: OverrideReading) -> Self {
self.override_reading = override_reading;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum VersionPolicy {
Pinned,
Banned,
Ignored,
SameRange,
SameMinor,
SnappedTo,
#[default]
PreferredSemver,
SemverRangeOnly,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct VersionGroup {
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub dependencies: Selector,
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub packages: Selector,
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub dependency_types: DependencyTypes,
pub specifier_types: Vec<String>,
pub policy: VersionPolicy,
#[serde(skip_serializing_if = "Option::is_none")]
pub pin: Option<String>,
pub snap_to: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prefer: Option<Prefer>,
}
impl VersionGroup {
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn with_dependencies(mut self, dependencies: Selector) -> Self {
self.dependencies = dependencies;
self
}
#[must_use]
pub fn with_packages(mut self, packages: Selector) -> Self {
self.packages = packages;
self
}
#[must_use]
pub fn with_dependency_types(mut self, dependency_types: DependencyTypes) -> Self {
self.dependency_types = dependency_types;
self
}
#[must_use]
pub fn with_specifier_types<I, S>(mut self, specifier_types: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.specifier_types = specifier_types.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub const fn with_policy(mut self, policy: VersionPolicy) -> Self {
self.policy = policy;
self
}
#[must_use]
pub fn with_pin(mut self, pin: impl Into<String>) -> Self {
self.pin = Some(pin.into());
self
}
#[must_use]
pub fn with_snap_to<I, S>(mut self, snap_to: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.snap_to = snap_to.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub const fn with_prefer(mut self, prefer: Prefer) -> Self {
self.prefer = Some(prefer);
self
}
fn inconsistency(&self) -> Option<&'static str> {
match self.policy {
VersionPolicy::Pinned if self.pin.is_none() => return Some("policy `pinned` requires `pin`"),
VersionPolicy::SnappedTo if self.snap_to.is_empty() => {
return Some("policy `snapped-to` requires `snap-to`");
}
_ => {}
}
if self.pin.is_some() && self.policy != VersionPolicy::Pinned {
return Some("`pin` is only valid with policy `pinned`");
}
if !self.snap_to.is_empty() && self.policy != VersionPolicy::SnappedTo {
return Some("`snap-to` is only valid with policy `snapped-to`");
}
if self.prefer.is_some() && self.policy != VersionPolicy::PreferredSemver {
return Some("`prefer` is only valid with policy `preferred-semver`");
}
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum SemverRange {
#[serde(rename = "")]
Exact,
#[serde(rename = "*")]
Any,
#[serde(rename = "^")]
Caret,
#[serde(rename = "~")]
Tilde,
#[serde(rename = ">")]
Gt,
#[serde(rename = ">=")]
Gte,
#[serde(rename = "<")]
Lt,
#[serde(rename = "<=")]
Lte,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct SemverGroup {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default)]
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub dependencies: Selector,
#[serde(default)]
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub packages: Selector,
#[serde(default)]
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub dependency_types: DependencyTypes,
#[serde(default)]
pub specifier_types: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub range: Option<SemverRange>,
#[serde(default, skip_serializing_if = "is_false")]
pub ignored: bool,
}
impl SemverGroup {
#[must_use]
pub fn new(range: SemverRange) -> Self {
Self {
label: None,
dependencies: Selector::default(),
packages: Selector::default(),
dependency_types: DependencyTypes::default(),
specifier_types: Vec::new(),
range: Some(range),
ignored: false,
}
}
#[must_use]
pub fn ignored() -> Self {
Self {
range: None,
ignored: true,
..Self::new(SemverRange::Exact)
}
}
fn inconsistency(&self) -> Option<&'static str> {
match (self.range, self.ignored) {
(None, false) => Some("set `range`, or `ignored = true` to exempt the dependencies from a range"),
(Some(_), true) => Some("`range` and `ignored = true` are mutually exclusive"),
_ => None,
}
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn with_dependencies(mut self, dependencies: Selector) -> Self {
self.dependencies = dependencies;
self
}
#[must_use]
pub fn with_packages(mut self, packages: Selector) -> Self {
self.packages = packages;
self
}
#[must_use]
pub fn with_dependency_types(mut self, dependency_types: DependencyTypes) -> Self {
self.dependency_types = dependency_types;
self
}
#[must_use]
pub fn with_specifier_types<I, S>(mut self, specifier_types: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.specifier_types = specifier_types.into_iter().map(Into::into).collect();
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum UpdateTarget {
#[default]
Latest,
Minor,
Patch,
}
impl UpdateTarget {
#[must_use]
pub const fn stricter(self, other: Self) -> Self {
match (self, other) {
(Self::Patch, _) | (_, Self::Patch) => Self::Patch,
(Self::Minor, _) | (_, Self::Minor) => Self::Minor,
_ => Self::Latest,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Latest => "latest",
Self::Minor => "minor",
Self::Patch => "patch",
}
}
}
pub const DEFAULT_MAX_CONCURRENT_REQUESTS: usize = 12;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Update {
#[serde(skip_serializing_if = "Option::is_none")]
pub minimum_release_age: Option<u64>,
pub max_concurrent_requests: usize,
}
impl Default for Update {
fn default() -> Self {
Self {
minimum_release_age: None,
max_concurrent_requests: DEFAULT_MAX_CONCURRENT_REQUESTS,
}
}
}
impl Update {
#[must_use]
pub const fn with_minimum_release_age(mut self, minutes: Option<u64>) -> Self {
self.minimum_release_age = minutes;
self
}
#[must_use]
pub const fn with_max_concurrent_requests(mut self, requests: usize) -> Self {
self.max_concurrent_requests = requests;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct UpdateGroup {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default)]
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub dependencies: Selector,
#[serde(default)]
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub packages: Selector,
#[serde(default)]
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub dependency_types: DependencyTypes,
#[serde(default)]
pub specifier_types: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<UpdateTarget>,
#[serde(default, skip_serializing_if = "is_false")]
pub ignored: bool,
}
impl UpdateGroup {
#[must_use]
pub fn new(target: UpdateTarget) -> Self {
Self {
label: None,
dependencies: Selector::default(),
packages: Selector::default(),
dependency_types: DependencyTypes::default(),
specifier_types: Vec::new(),
target: Some(target),
ignored: false,
}
}
#[must_use]
pub fn ignored() -> Self {
Self {
target: None,
ignored: true,
..Self::new(UpdateTarget::Latest)
}
}
fn inconsistency(&self) -> Option<&'static str> {
match (self.target, self.ignored) {
(None, false) => Some("set `target`, or `ignored = true` to leave the dependencies alone"),
(Some(_), true) => Some("`target` and `ignored = true` are mutually exclusive"),
_ => None,
}
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn with_dependencies(mut self, dependencies: Selector) -> Self {
self.dependencies = dependencies;
self
}
#[must_use]
pub fn with_packages(mut self, packages: Selector) -> Self {
self.packages = packages;
self
}
#[must_use]
pub fn with_dependency_types(mut self, dependency_types: DependencyTypes) -> Self {
self.dependency_types = dependency_types;
self
}
#[must_use]
pub fn with_specifier_types<I, S>(mut self, specifier_types: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.specifier_types = specifier_types.into_iter().map(Into::into).collect();
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum CustomStrategy {
#[serde(rename = "name@version")]
NameAtVersion,
#[serde(rename = "version")]
Version,
#[serde(rename = "versions-by-name", alias = "versionsByName")]
VersionsByName,
#[serde(rename = "name~version")]
NameTildeVersion,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct CustomType {
pub name: String,
pub path: String,
pub strategy: CustomStrategy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name_path: Option<String>,
}
impl CustomType {
#[must_use]
pub fn new(name: impl Into<String>, path: impl Into<String>, strategy: CustomStrategy) -> Self {
Self {
name: name.into(),
path: path.into(),
strategy,
name_path: None,
}
}
#[must_use]
pub fn with_name_path(mut self, name_path: impl Into<String>) -> Self {
self.name_path = Some(name_path.into());
self
}
pub fn name_segments(&self) -> impl Iterator<Item = &str> {
self.name_path.iter().flat_map(|path| path.split('.'))
}
pub fn segments(&self) -> impl Iterator<Item = &str> {
self.path.split('.')
}
fn inconsistency(&self) -> Option<String> {
if self.name.is_empty() || self.name.starts_with('!') {
return Some("`name` must not be empty or start with `!`".to_owned());
}
if DependencyType::is_builtin_token(&self.name) {
return Some(format!(
"`name` \"{}\" is the name of a built-in dependency type",
self.name
));
}
if self.segments().any(str::is_empty) {
return Some(format!(
"`path` \"{}\" must be a dotted path of non-empty keys",
self.path
));
}
let takes_name_path = self.strategy == CustomStrategy::NameTildeVersion;
match (takes_name_path, self.name_path.is_some()) {
(true, false) => return Some("strategy `name~version` requires `name-path`".to_owned()),
(false, true) => return Some("`name-path` is only valid with strategy `name~version`".to_owned()),
_ => {}
}
if self.name_segments().any(str::is_empty) {
return Some("`name-path` must be a dotted path of non-empty keys".to_owned());
}
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum CatalogMode {
#[default]
Off,
Prefer,
Enforce,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Catalog {
pub mode: CatalogMode,
pub min_packages: usize,
pub exclude: Vec<String>,
pub report_unused_entries: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub extend_exclude: Vec<String>,
}
impl Default for Catalog {
fn default() -> Self {
Self {
mode: CatalogMode::Off,
min_packages: 2,
exclude: Vec::new(),
report_unused_entries: false,
extend_exclude: Vec::new(),
}
}
}
impl Catalog {
#[must_use]
pub const fn with_mode(mut self, mode: CatalogMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub const fn with_min_packages(mut self, min_packages: usize) -> Self {
self.min_packages = min_packages;
self
}
#[must_use]
pub fn with_exclude<I, S>(mut self, exclude: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.exclude = exclude.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub const fn with_report_unused_entries(mut self, report_unused_entries: bool) -> Self {
self.report_unused_entries = report_unused_entries;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct WorkspaceProtocol {
pub internal: bool,
pub allow_local_paths: bool,
}
impl WorkspaceProtocol {
#[must_use]
pub const fn with_internal(mut self, internal: bool) -> Self {
self.internal = internal;
self
}
#[must_use]
pub const fn with_allow_local_paths(mut self, allow_local_paths: bool) -> Self {
self.allow_local_paths = allow_local_paths;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Format {
pub sort_first: Vec<String>,
pub sort_az: Vec<String>,
pub sort_exports: Vec<String>,
pub sort_packages: bool,
pub format_bugs: bool,
pub format_repository: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub indent: Option<String>,
}
fn strings(items: &[&str]) -> Vec<String> {
items.iter().map(|item| (*item).to_owned()).collect()
}
impl Default for Format {
fn default() -> Self {
Self {
sort_first: strings(&["name", "description", "version", "author"]),
sort_az: strings(&[
"bin",
"contributors",
"dependencies",
"devDependencies",
"keywords",
"peerDependencies",
"resolutions",
"scripts",
]),
sort_exports: strings(&["types", "default"]),
sort_packages: true,
format_bugs: false,
format_repository: false,
indent: None,
}
}
}
impl Format {
#[must_use]
pub fn with_sort_first<I: IntoIterator<Item = S>, S: Into<String>>(mut self, keys: I) -> Self {
self.sort_first = keys.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_sort_az<I: IntoIterator<Item = S>, S: Into<String>>(mut self, keys: I) -> Self {
self.sort_az = keys.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_sort_exports<I: IntoIterator<Item = S>, S: Into<String>>(mut self, keys: I) -> Self {
self.sort_exports = keys.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub const fn with_sort_packages(mut self, sort: bool) -> Self {
self.sort_packages = sort;
self
}
#[must_use]
pub const fn with_format_bugs(mut self, format: bool) -> Self {
self.format_bugs = format;
self
}
#[must_use]
pub const fn with_format_repository(mut self, format: bool) -> Self {
self.format_repository = format;
self
}
#[must_use]
pub fn with_indent(mut self, indent: impl Into<String>) -> Self {
self.indent = Some(indent.into());
self
}
#[must_use]
pub fn indent_text(&self) -> Option<String> {
self.indent.as_ref().map(|indent| indent.replace("\\t", "\t"))
}
fn inconsistency(&self) -> Option<String> {
let indent = self.indent_text()?;
(indent.is_empty() || indent.chars().any(|c| c != ' ' && c != '\t')).then(|| {
format!(
"indent: {:?} is not a run of spaces or tabs",
self.indent.as_deref().unwrap_or_default()
)
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[non_exhaustive]
pub struct DependencyFamily {
pub name: String,
#[serde(default)]
pub packages: Vec<String>,
#[serde(default = "default_true")]
pub enabled: bool,
}
impl DependencyFamily {
#[must_use]
pub fn new<I, S>(name: impl Into<String>, packages: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self {
name: name.into(),
packages: packages.into_iter().map(Into::into).collect(),
enabled: true,
}
}
#[must_use]
pub const fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
}
const fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum JsDiscovery {
#[default]
Auto,
Git,
Walk,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct JsCycles {
pub allow: Vec<Vec<String>>,
pub dynamic_imports: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
#[allow(
clippy::struct_excessive_bools,
reason = "each bool is an independent switch of the analysis"
)]
pub struct Js {
pub enabled: bool,
pub entry: Vec<String>,
pub project: Vec<String>,
pub ignore: Vec<String>,
pub ignore_files: Vec<String>,
pub ignore_dependencies: Vec<String>,
pub ignore_binaries: Vec<String>,
pub ignore_global_binaries: bool,
pub ignore_unresolved: Vec<String>,
pub ignore_workspaces: Vec<String>,
pub ignore_exports_used_in_file: IgnoreExportsUsedInFile,
pub ignore_members: Vec<String>,
pub production: bool,
#[serde(skip)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub isolate_workspaces: bool,
#[serde(skip)]
#[cfg_attr(feature = "schema", schemars(skip))]
pub no_gitignore: bool,
pub include_entry_exports: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub tsconfig: Option<String>,
pub use_tsconfig_files: bool,
pub include_fixtures: bool,
pub discovery: JsDiscovery,
pub cycles: JsCycles,
pub workspaces: IndexMap<String, JsWorkspace>,
pub plugins: IndexMap<String, PluginConfig>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub extend_ignore: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub extend_ignore_dependencies: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub extend_ignore_binaries: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub extend_ignore_unresolved: Vec<String>,
}
impl Js {
#[must_use]
pub fn with_entry<I, S>(mut self, entry: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.entry = entry.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_project<I, S>(mut self, project: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.project = project.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub const fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
#[must_use]
pub const fn with_production(mut self, production: bool) -> Self {
self.production = production;
self
}
#[must_use]
pub const fn with_isolate_workspaces(mut self, isolate: bool) -> Self {
self.isolate_workspaces = isolate;
self
}
#[must_use]
pub fn with_tsconfig(mut self, path: impl Into<String>) -> Self {
self.tsconfig = Some(path.into());
self
}
#[must_use]
pub const fn with_use_tsconfig_files(mut self, enabled: bool) -> Self {
self.use_tsconfig_files = enabled;
self
}
#[must_use]
pub fn with_plugin(mut self, name: impl Into<String>, plugin: PluginConfig) -> Self {
self.plugins.insert(name.into(), plugin);
self
}
}
impl Default for Js {
fn default() -> Self {
Self {
enabled: true,
entry: Vec::new(),
project: Vec::new(),
ignore: Vec::new(),
ignore_files: Vec::new(),
ignore_dependencies: Vec::new(),
ignore_binaries: Vec::new(),
ignore_global_binaries: true,
ignore_unresolved: Vec::new(),
ignore_workspaces: Vec::new(),
ignore_exports_used_in_file: IgnoreExportsUsedInFile::default(),
ignore_members: Vec::new(),
production: false,
isolate_workspaces: false,
no_gitignore: false,
include_entry_exports: false,
tsconfig: None,
use_tsconfig_files: false,
include_fixtures: false,
discovery: JsDiscovery::Auto,
cycles: JsCycles::default(),
workspaces: IndexMap::new(),
plugins: IndexMap::new(),
extend_ignore: Vec::new(),
extend_ignore_dependencies: Vec::new(),
extend_ignore_binaries: Vec::new(),
extend_ignore_unresolved: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
#[non_exhaustive]
pub enum IgnoreExportsUsedInFile {
All(bool),
Kinds(ExportKinds),
}
impl<'de> Deserialize<'de> for IgnoreExportsUsedInFile {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = IgnoreExportsUsedInFile;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("`true`, `false` or a table of export kinds")
}
fn visit_bool<E: serde::de::Error>(self, value: bool) -> Result<Self::Value, E> {
Ok(IgnoreExportsUsedInFile::All(value))
}
fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
ExportKinds::deserialize(serde::de::value::MapAccessDeserializer::new(map))
.map(IgnoreExportsUsedInFile::Kinds)
}
}
deserializer.deserialize_any(Visitor)
}
}
impl Default for IgnoreExportsUsedInFile {
fn default() -> Self {
Self::All(false)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
#[allow(clippy::struct_excessive_bools)]
pub struct ExportKinds {
pub variable: bool,
pub function: bool,
pub class: bool,
#[serde(rename = "enum")]
pub enumeration: bool,
pub namespace: bool,
#[serde(rename = "type")]
pub type_alias: bool,
pub interface: bool,
pub member: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct JsWorkspace {
pub entry: Vec<String>,
pub project: Vec<String>,
pub ignore: Vec<String>,
pub ignore_files: Vec<String>,
pub ignore_dependencies: Vec<String>,
pub ignore_binaries: Vec<String>,
pub ignore_unresolved: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_entry_exports: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_exports_used_in_file: Option<IgnoreExportsUsedInFile>,
pub ignore_members: Vec<String>,
pub paths: IndexMap<String, Vec<String>>,
pub plugins: IndexMap<String, PluginConfig>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
#[non_exhaustive]
pub enum PluginConfig {
Enabled(bool),
Options(PluginOptions),
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct PluginOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
pub config: Vec<String>,
pub entry: Vec<String>,
pub project: Vec<String>,
}
impl PluginOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_enabled(mut self, enabled: bool) -> Self {
self.enabled = Some(enabled);
self
}
#[must_use]
pub fn with_config<I, S>(mut self, config: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.config = config.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_entry<I, S>(mut self, entry: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.entry = entry.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_project<I, S>(mut self, project: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.project = project.into_iter().map(Into::into).collect();
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct PackageJs {
#[serde(skip_serializing_if = "Option::is_none")]
pub entry: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub project: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_dependencies: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_binaries: Option<Vec<String>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub extend_ignore: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub extend_ignore_dependencies: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub extend_ignore_binaries: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_unresolved: Option<Vec<String>>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub extend_ignore_unresolved: Vec<String>,
#[serde(skip_serializing_if = "IndexMap::is_empty")]
pub plugins: IndexMap<String, PluginConfig>,
}
impl PackageJs {
#[must_use]
pub fn is_empty(&self) -> bool {
self == &Self::default()
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
#[non_exhaustive]
pub struct Override {
#[serde(skip_serializing_if = "Selector::is_empty")]
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub packages: Selector,
#[serde(skip_serializing_if = "Selector::is_empty")]
#[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
pub files: Selector,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_level: Option<Level>,
#[serde(with = "rules_map", skip_serializing_if = "IndexMap::is_empty")]
#[cfg_attr(feature = "schema", schemars(with = "IndexMap<String, RuleConfigSchema>"))]
pub rules: IndexMap<String, RuleConfig>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub ignore: Vec<IgnoreEntry>,
#[serde(skip_serializing_if = "PackageJs::is_empty")]
pub js: PackageJs,
#[serde(skip)]
pub scope: Option<OverrideScope>,
}
impl Override {
pub(crate) fn inconsistency(&self) -> Option<&'static str> {
if self.packages.is_empty() && self.files.is_empty() {
return Some(
"set at least one of `packages` and `files`; an override without a selector would apply everywhere",
);
}
if !self.js.is_empty() && (self.packages.is_empty() || !self.files.is_empty()) {
return Some("`js` needs `packages` and cannot be combined with `files`");
}
if self.rules.values().any(|rule| !rule.options.is_empty()) {
return Some("rule options are root-scoped; an override can only set rule levels");
}
None
}
#[must_use]
pub fn applies_to(&self, package: Option<(&str, &Path)>, file: Option<&Path>) -> bool {
self.scope.as_ref().is_some_and(|scope| scope.applies_to(package, file))
}
#[must_use]
pub fn applies_to_slashed(&self, package: Option<(&str, &str)>, file: Option<&str>) -> bool {
self.scope
.as_ref()
.is_some_and(|scope| scope.applies_to_slashed(package, file))
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct OverrideScope {
pub declared_in: PathBuf,
pub source: usize,
has_packages: bool,
has_files: bool,
packages: Selector,
files: Selector,
}
impl OverrideScope {
pub fn new(scoped: &Override, declared_in: &Path, source: usize, root: &Path) -> Result<Self, ConfigError> {
Ok(Self {
declared_in: declared_in.to_path_buf(),
source,
has_packages: !scoped.packages.is_empty(),
has_files: !scoped.files.is_empty(),
packages: anchor_patterns(scoped.packages.patterns(), declared_in, root, false)?,
files: anchor_patterns(scoped.files.patterns(), declared_in, root, true)?,
})
}
fn applies_to(&self, package: Option<(&str, &Path)>, file: Option<&Path>) -> bool {
let package = package.map(|(name, dir)| (name, skif_core::slash(dir)));
let file = file.map(skif_core::slash);
self.applies_to_slashed(
package.as_ref().map(|(name, dir)| (*name, dir.as_str())),
file.as_deref(),
)
}
fn applies_to_slashed(&self, package: Option<(&str, &str)>, file: Option<&str>) -> bool {
let package_ok =
!self.has_packages || package.is_some_and(|(name, dir)| self.packages.matches_any([name, dir], true));
let file_ok = !self.has_files || file.is_some_and(|file| self.files.matches(file, false));
package_ok && file_ok
}
}