1use std::path::{Path, PathBuf};
2
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use skif_core::{FileSystem, OsFs};
6use skif_specifier::CONFIG_IDENTIFIERS;
7
8use crate::dependency_types::{DependencyType, DependencyTypes};
9use crate::error::ConfigError;
10use crate::overrides::ConfigOverrides;
11use crate::rules::{Level, RuleConfig, rules_map};
12use crate::selector::{Selector, anchor_patterns, unanchored};
13
14#[cfg(feature = "schema")]
15use crate::schema::RuleConfigSchema;
16
17#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
21#[non_exhaustive]
22pub struct Config {
23 #[serde(skip_serializing_if = "is_false")]
26 pub root: bool,
27 #[serde(skip_serializing_if = "Vec::is_empty")]
29 pub extends: Vec<String>,
30 #[serde(skip_serializing_if = "Vec::is_empty")]
32 pub reset: Vec<ResetKey>,
33 pub workspace: Workspace,
35 pub lint: Lint,
37 #[serde(with = "rules_map")]
39 #[cfg_attr(feature = "schema", schemars(with = "IndexMap<String, RuleConfigSchema>"))]
40 pub rules: IndexMap<String, RuleConfig>,
41 pub ignore: Vec<IgnoreEntry>,
43 pub policy: Policy,
45 pub version_groups: Vec<VersionGroup>,
47 pub semver_groups: Vec<SemverGroup>,
49 #[serde(skip_serializing_if = "Vec::is_empty")]
51 pub custom_types: Vec<CustomType>,
52 pub catalog: Catalog,
54 pub workspace_protocol: WorkspaceProtocol,
56 pub dependency_families: Vec<DependencyFamily>,
58 pub update: Update,
60 #[serde(skip_serializing_if = "Vec::is_empty")]
62 pub update_groups: Vec<UpdateGroup>,
63 pub js: Js,
65 pub format: Format,
67 #[serde(skip_serializing_if = "Vec::is_empty")]
69 pub overrides: Vec<Override>,
70 #[serde(skip)]
73 #[cfg_attr(feature = "schema", schemars(skip))]
74 pub cli_levels: IndexMap<String, Level>,
75 #[serde(skip)]
78 #[cfg_attr(feature = "schema", schemars(skip))]
79 pub filter: InstanceFilter,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Default)]
86#[non_exhaustive]
87pub struct InstanceFilter {
88 pub dependencies: Selector,
90 pub dependency_types: DependencyTypes,
93 pub specifier_types: Vec<String>,
95 pub sources: Selector,
97}
98
99impl InstanceFilter {
100 pub fn new(
106 dependencies: &[String],
107 dependency_types: &[String],
108 specifier_types: &[String],
109 sources: &[String],
110 ) -> Result<Self, ConfigError> {
111 if let Some(message) = unknown_specifier_type(specifier_types) {
112 return Err(ConfigError::invalid(None, format!("--specifier-types: {message}")));
113 }
114 Ok(Self {
115 dependencies: Selector::new(dependencies.iter().cloned())?,
116 dependency_types: DependencyTypes::new(dependency_types)?,
117 specifier_types: specifier_types.to_vec(),
118 sources: Selector::new(sources.iter().cloned())?,
119 })
120 }
121
122 #[must_use]
124 pub fn is_empty(&self) -> bool {
125 self.dependencies.is_empty()
126 && self.dependency_types.is_empty()
127 && self.specifier_types.is_empty()
128 && self.sources.is_empty()
129 }
130
131 #[must_use]
133 pub fn unknown_custom_type<'a>(&'a self, config: &Config) -> Option<&'a str> {
134 self.dependency_types
135 .custom_names()
136 .find(|name| config.custom_types.iter().all(|custom| custom.name != *name))
137 }
138}
139
140#[allow(clippy::trivially_copy_pass_by_ref)]
141const fn is_false(value: &bool) -> bool {
142 !*value
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
148#[serde(rename_all = "kebab-case")]
149#[non_exhaustive]
150pub enum ResetKey {
151 VersionGroups,
153 SemverGroups,
155 Ignore,
157 DependencyFamilies,
159 CustomTypes,
161 Overrides,
163 UpdateGroups,
165}
166
167impl ResetKey {
168 #[must_use]
170 pub const fn name(self) -> &'static str {
171 match self {
172 Self::VersionGroups => "version-groups",
173 Self::SemverGroups => "semver-groups",
174 Self::Ignore => "ignore",
175 Self::DependencyFamilies => "dependency-families",
176 Self::CustomTypes => "custom-types",
177 Self::Overrides => "overrides",
178 Self::UpdateGroups => "update-groups",
179 }
180 }
181}
182
183fn selector_problem(patterns: &[String]) -> Option<String> {
185 match Selector::new(patterns.iter().cloned()) {
186 Err(ConfigError::Invalid { message, .. }) => Some(message),
187 _ => None,
188 }
189}
190
191impl Config {
192 pub fn from_toml_str(source: &str) -> Result<Self, ConfigError> {
198 Self::parse(source, None)
199 }
200
201 pub fn from_toml_file(path: &Path) -> Result<Self, ConfigError> {
206 Self::from_toml_file_with_fs(path, &OsFs)
207 }
208
209 pub fn from_toml_file_with_fs(path: &Path, fs: &dyn FileSystem) -> Result<Self, ConfigError> {
214 let source = fs.read_to_string(path).map_err(|source| ConfigError::Io {
215 path: path.to_path_buf(),
216 source,
217 })?;
218 Self::parse(&source, Some(path))
219 }
220
221 pub(crate) fn from_document(
226 document: toml::Spanned<toml::de::DeTable<'_>>,
227 source: &str,
228 path: Option<&Path>,
229 resolve_references: bool,
230 ) -> Result<Self, ConfigError> {
231 let config = Self::deserialize(toml::de::Deserializer::from(document))
232 .map_err(|err| ConfigError::from_toml(&err, source, path))?;
233 config.validate_with(path, resolve_references)?;
234 Ok(config)
235 }
236
237 fn parse(source: &str, path: Option<&Path>) -> Result<Self, ConfigError> {
240 let from_toml = |err: &toml::de::Error| ConfigError::from_toml(err, source, path);
241 let document = toml::de::DeTable::parse(source).map_err(|err| from_toml(&err))?;
242 let config = Self::from_document(document.clone(), source, path, true)?;
243 let table = toml::Table::deserialize(toml::de::Deserializer::from(document)).map_err(|err| from_toml(&err))?;
244 if !crate::merge::has_extend_keys(&table) {
245 return Ok(config);
246 }
247 let mut merger = crate::merge::Merger::default();
248 merger.apply(0, &table, &skif_core::HashMap::default());
249 let folded = merger.into_table();
250 toml::Value::Table(folded)
251 .try_into()
252 .map_err(|err: toml::de::Error| ConfigError::invalid(path, err.message()))
253 }
254
255 #[must_use]
265 pub fn missing_ignore_reason(&self) -> Option<String> {
266 unreasoned_entry("ignore", &self.ignore)
267 .or_else(|| {
268 self.js_ignore_lists().into_iter().find_map(|(key, items)| {
269 items
270 .iter()
271 .enumerate()
272 .find(|(_, item)| item.reason.is_none())
273 .map(|(index, item)| format!("{key}[{index}] `{}`", item.name))
274 })
275 })
276 .or_else(|| {
277 self.overrides
278 .iter()
279 .enumerate()
280 .find_map(|(index, scoped)| unreasoned_entry(&format!("overrides[{index}].ignore"), &scoped.ignore))
281 })
282 .or_else(|| self.unreasoned_allow_entry())
283 }
284
285 fn unreasoned_allow_entry(&self) -> Option<String> {
289 self.rules.iter().find_map(|(id, rule)| {
290 let items = rule.options.get("allow")?.as_array()?;
291 items.iter().enumerate().find_map(|(index, item)| {
292 let name = match item {
293 toml::Value::String(name) => name.as_str(),
294 toml::Value::Table(table) if !table.contains_key("reason") => {
295 table.get("name").and_then(toml::Value::as_str).unwrap_or_default()
296 }
297 _ => return None,
298 };
299 Some(format!("rules.{id}.allow[{index}] `{name}`"))
300 })
301 })
302 }
303
304 fn js_ignore_lists(&self) -> Vec<(String, &[IgnoreItem])> {
307 let mut lists = keyed("js", &self.js.ignore_lists());
308 for (dir, workspace) in &self.js.workspaces {
309 lists.extend(keyed(&format!("js.workspaces.\"{dir}\""), &workspace.ignore_lists()));
310 }
311 for (index, scoped) in self.overrides.iter().enumerate() {
312 lists.extend(keyed(&format!("overrides[{index}].js"), &scoped.js.ignore_lists()));
313 }
314 lists
315 }
316
317 pub fn to_toml_string(&self) -> Result<String, toml::ser::Error> {
322 toml::to_string(self)
323 }
324
325 pub fn validate(&self) -> Result<(), ConfigError> {
330 self.validate_with(None, true)
331 }
332
333 fn validate_with(&self, path: Option<&Path>, resolve_references: bool) -> Result<(), ConfigError> {
334 let invalid = |message: String| Err(ConfigError::invalid(path, message));
335 for (index, group) in self.version_groups.iter().enumerate() {
336 if let Some(message) = group.inconsistency() {
337 return invalid(format!("version-groups[{index}]: {message}"));
338 }
339 if let Some(message) = unknown_specifier_type(&group.specifier_types) {
340 return invalid(format!("version-groups[{index}]: {message}"));
341 }
342 }
343 for (index, group) in self.semver_groups.iter().enumerate() {
344 if let Some(message) = group.inconsistency() {
345 return invalid(format!("semver-groups[{index}]: {message}"));
346 }
347 if let Some(message) = unknown_specifier_type(&group.specifier_types) {
348 return invalid(format!("semver-groups[{index}]: {message}"));
349 }
350 }
351 for (index, group) in self.update_groups.iter().enumerate() {
352 if let Some(message) = group.inconsistency() {
353 return invalid(format!("update-groups[{index}]: {message}"));
354 }
355 if let Some(message) = unknown_specifier_type(&group.specifier_types) {
356 return invalid(format!("update-groups[{index}]: {message}"));
357 }
358 }
359 if self.update.max_concurrent_requests == 0 {
360 return invalid("update.max-concurrent-requests must be at least 1".to_string());
361 }
362 if let Some(message) = self.custom_type_problem(resolve_references) {
363 return invalid(message);
364 }
365 for (index, entry) in self.ignore.iter().enumerate() {
366 if entry.rules.is_empty()
367 && entry.packages.is_empty()
368 && entry.dependencies.is_empty()
369 && entry.files.is_empty()
370 {
371 return invalid(format!(
372 "ignore[{index}]: set at least one of `rules`, `packages`, `dependencies` or `files`; an entry without filters would suppress every diagnostic"
373 ));
374 }
375 if entry.reason.as_deref().is_some_and(|reason| reason.trim().is_empty()) {
376 return invalid(format!("ignore[{index}]: `reason` is empty; say why or remove it"));
377 }
378 }
379 for (key, items) in self.js_ignore_lists() {
380 let blank = items
381 .iter()
382 .position(|item| item.reason.as_deref().is_some_and(|reason| reason.trim().is_empty()));
383 if let Some(index) = blank {
384 return invalid(format!("{key}[{index}]: `reason` is empty; say why or remove it"));
385 }
386 }
387 if let Some(message) = self.js.compilers.iter().find_map(|(extension, compiler)| {
388 compiler.problem(&crate::provenance::join_key("js.compilers", extension), extension)
389 }) {
390 return invalid(message);
391 }
392 if let Some(message) = self.format.inconsistency() {
393 return invalid(format!("format.{message}"));
394 }
395 for (index, scoped) in self.overrides.iter().enumerate() {
396 if let Some(message) = scoped.inconsistency() {
397 return invalid(format!("overrides[{index}]: {message}"));
398 }
399 }
400 if self.catalog.min_packages == 0 {
401 return invalid("catalog.min-packages must be at least 1".to_string());
402 }
403 if let Some(message) = selector_problem(&self.catalog.exclude) {
404 return invalid(format!("catalog.exclude: {message}"));
405 }
406 for (index, pattern) in self.workspace.discover_ignore.iter().enumerate() {
407 let glob = pattern.strip_prefix('!').unwrap_or(pattern).trim_start_matches('/');
409 let mut builder = ignore::gitignore::GitignoreBuilder::new("");
410 let checked = builder
411 .add_line(None, pattern)
412 .map(|_| ())
413 .map_err(|err| err.to_string())
414 .and_then(|()| globset::Glob::new(glob).map(|_| ()).map_err(|err| err.to_string()));
415 if let Err(err) = checked {
416 return invalid(format!("workspace.discover-ignore[{index}]: {err}"));
417 }
418 }
419 for (index, family) in self.dependency_families.iter().enumerate() {
420 if family.name.is_empty() {
421 return invalid(format!("dependency-families[{index}]: `name` must not be empty"));
422 }
423 if family.enabled && family.packages.is_empty() {
424 return invalid(format!("dependency-families[{index}]: `packages` must not be empty"));
425 }
426 if let Some(message) = selector_problem(&family.packages) {
427 return invalid(format!("dependency-families[{index}]: {message}"));
428 }
429 match family.anchor.as_deref() {
430 Some(_) if family.scope.is_manifests() => {
431 return invalid(format!(
432 "dependency-families[{index}]: `anchor` is only valid with `scope = \"importer\"` or `scope = \"lockfile\"`"
433 ));
434 }
435 Some(anchor) if anchor.trim().is_empty() => {
436 return invalid(format!("dependency-families[{index}]: `anchor` must not be empty"));
437 }
438 _ => {}
439 }
440 }
441 Ok(())
442 }
443
444 fn custom_type_problem(&self, resolve_references: bool) -> Option<String> {
447 for (index, custom) in self.custom_types.iter().enumerate() {
448 if let Some(message) = custom.inconsistency() {
449 return Some(format!("custom-types[{index}]: {message}"));
450 }
451 if self.custom_types[..index]
452 .iter()
453 .any(|earlier| earlier.name == custom.name)
454 {
455 return Some(format!(
456 "custom-types[{index}]: the name \"{}\" is defined twice",
457 custom.name
458 ));
459 }
460 }
461 if !resolve_references {
462 return None;
463 }
464 let lists = std::iter::once(("policy.dependency-types".to_owned(), &self.policy.dependency_types))
465 .chain(self.version_groups.iter().enumerate().map(|(index, group)| {
466 (
467 format!("version-groups[{index}].dependency-types"),
468 &group.dependency_types,
469 )
470 }))
471 .chain(self.semver_groups.iter().enumerate().map(|(index, group)| {
472 (
473 format!("semver-groups[{index}].dependency-types"),
474 &group.dependency_types,
475 )
476 }))
477 .chain(self.update_groups.iter().enumerate().map(|(index, group)| {
478 (
479 format!("update-groups[{index}].dependency-types"),
480 &group.dependency_types,
481 )
482 }));
483 for (place, types) in lists {
484 if let Some(name) = types
485 .custom_names()
486 .find(|name| self.custom_types.iter().all(|custom| custom.name != *name))
487 {
488 let hint = closest(
489 name,
490 &self
491 .custom_types
492 .iter()
493 .map(|custom| custom.name.as_str())
494 .chain(BUILTIN_TYPE_TOKENS)
495 .collect::<Vec<_>>(),
496 )
497 .map_or_else(String::new, |s| format!("; did you mean \"{s}\"?"));
498 return Some(format!(
499 "{place}: unknown dependency type \"{name}\"{hint} (expected {}, or the name of a `[[custom-types]]` entry)",
500 BUILTIN_TYPE_TOKENS.join(", ")
501 ));
502 }
503 }
504 None
505 }
506
507 pub fn validate_rules(&self, known: &[&str]) -> Result<(), ConfigError> {
512 let configured = self.rules.keys();
513 let ignored = self.ignore.iter().flat_map(|entry| entry.rules.iter());
514 let scoped = self.overrides.iter().flat_map(|scoped| {
515 scoped
516 .rules
517 .keys()
518 .chain(scoped.ignore.iter().flat_map(|entry| entry.rules.iter()))
519 });
520 for id in configured.chain(ignored).chain(scoped) {
521 if !known.contains(&id.as_str()) {
522 return Err(ConfigError::UnknownRule {
523 rule: id.clone(),
524 suggestion: closest(id, known),
525 });
526 }
527 }
528 Ok(())
529 }
530
531 pub fn validate_plugins(&self, known: &[&str]) -> Result<(), ConfigError> {
537 let workspaces = self
538 .js
539 .workspaces
540 .iter()
541 .map(|(dir, workspace)| (format!("js.workspaces.\"{dir}\".plugins"), &workspace.plugins));
542 let overrides = self
543 .overrides
544 .iter()
545 .enumerate()
546 .map(|(index, scoped)| (format!("overrides[{index}].js.plugins"), &scoped.js.plugins));
547 let tables = std::iter::once(("js.plugins".to_owned(), &self.js.plugins))
548 .chain(workspaces)
549 .chain(overrides);
550 for (key, table) in tables {
551 if let Some(id) = table.keys().find(|id| !known.contains(&id.as_str())) {
552 let hint = closest(id, known).map_or_else(String::new, |close| format!("; did you mean `{close}`?"));
553 return Err(ConfigError::invalid(
554 None,
555 format!("{key}.{id}: unknown plugin `{id}`{hint}"),
556 ));
557 }
558 }
559 Ok(())
560 }
561
562 #[must_use]
564 pub fn rule(&self, id: &str) -> Option<&RuleConfig> {
565 self.rules.get(id)
566 }
567
568 pub fn apply(&mut self, overrides: &ConfigOverrides) -> Result<(), ConfigError> {
574 let ignore_packages = Selector::new(overrides.ignore_packages.iter().cloned())?;
575 let ignore_dependencies = Selector::new(overrides.ignore_dependencies.iter().cloned())?;
576 for (id, level) in &overrides.rules {
577 self.rules.entry(id.clone()).or_default().severity = Some(*level);
578 self.cli_levels.insert(id.clone(), *level);
579 }
580 for id in &overrides.ignore_rules {
581 self.rules.entry(id.clone()).or_default().severity = Some(Level::Off);
582 self.cli_levels.insert(id.clone(), Level::Off);
583 }
584 if !ignore_packages.is_empty() {
585 self.workspace.ignore_packages = ignore_packages;
586 }
587 if !ignore_dependencies.is_empty() {
588 self.ignore.push(IgnoreEntry {
589 dependencies: ignore_dependencies,
590 reason: Some("--ignore-dependency".to_owned()),
591 ..IgnoreEntry::default()
592 });
593 }
594 if let Some(value) = overrides.fail_on_warnings {
595 self.lint.fail_on_warnings = value;
596 }
597 if let Some(value) = overrides.production {
598 self.js.production = value;
599 }
600 if overrides.no_js {
601 self.js.enabled = false;
602 }
603 Ok(())
604 }
605}
606
607const BUILTIN_TYPE_TOKENS: [&str; 8] = [
609 "prod",
610 "dev",
611 "peer",
612 "optional",
613 "local",
614 "resolutions",
615 "overrides",
616 "pnpm-overrides",
617];
618
619fn unknown_specifier_type(specifier_types: &[String]) -> Option<String> {
621 specifier_types.iter().find_map(|raw| {
622 let name = raw.strip_prefix('!').unwrap_or(raw);
623 if CONFIG_IDENTIFIERS.contains(&name) {
624 return None;
625 }
626 let hint = closest(name, &CONFIG_IDENTIFIERS).map_or_else(String::new, |s| format!("; did you mean \"{s}\"?"));
627 Some(format!(
628 "unknown specifier type \"{name}\"{hint} (expected one of: {})",
629 CONFIG_IDENTIFIERS.join(", ")
630 ))
631 })
632}
633
634fn closest(id: &str, known: &[&str]) -> Option<String> {
635 let limit = (id.chars().count() / 3).clamp(2, 4);
636 known
637 .iter()
638 .map(|candidate| (strsim::levenshtein(id, candidate), candidate))
639 .filter(|(distance, _)| *distance <= limit)
640 .min_by_key(|(distance, _)| *distance)
641 .map(|(_, candidate)| (*candidate).to_string())
642}
643
644#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
646#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
647#[serde(rename_all = "kebab-case")]
648#[non_exhaustive]
649pub enum PackageManager {
650 Npm,
652 Pnpm,
654 Yarn,
656 Bun,
658}
659
660pub const FIXTURE_DIRECTORIES: [&str; 2] = ["__fixtures__", "fixtures"];
665
666pub const DEFAULT_DISCOVER_IGNORE: [&str; 4] = ["__fixtures__", "fixtures", "__mocks__", "__snapshots__"];
669
670fn default_discover_ignore() -> Vec<String> {
671 DEFAULT_DISCOVER_IGNORE.iter().map(|name| (*name).to_owned()).collect()
672}
673
674#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
676#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
677#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
678#[non_exhaustive]
679pub struct Workspace {
680 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
682 pub ignore_packages: Selector,
683 #[serde(skip_serializing_if = "Option::is_none")]
685 pub package_manager: Option<PackageManager>,
686 pub discover_ignore: Vec<String>,
692 #[serde(skip_serializing_if = "Selector::is_empty")]
694 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
695 pub extend_ignore_packages: Selector,
696}
697
698impl Default for Workspace {
699 fn default() -> Self {
700 Self {
701 ignore_packages: Selector::default(),
702 extend_ignore_packages: Selector::default(),
703 package_manager: None,
704 discover_ignore: default_discover_ignore(),
705 }
706 }
707}
708
709impl Workspace {
710 #[must_use]
712 pub fn with_ignore_packages(mut self, ignore_packages: Selector) -> Self {
713 self.ignore_packages = ignore_packages;
714 self
715 }
716
717 #[must_use]
719 pub const fn with_package_manager(mut self, package_manager: PackageManager) -> Self {
720 self.package_manager = Some(package_manager);
721 self
722 }
723
724 #[must_use]
726 pub fn with_discover_ignore(mut self, discover_ignore: Vec<String>) -> Self {
727 self.discover_ignore = discover_ignore;
728 self
729 }
730}
731
732fn keyed<'c>(prefix: &str, lists: &[(&str, &'c [IgnoreItem])]) -> Vec<(String, &'c [IgnoreItem])> {
734 lists
735 .iter()
736 .map(|(key, items)| (format!("{prefix}.{key}"), *items))
737 .collect()
738}
739
740fn unreasoned_entry(key: &str, entries: &[IgnoreEntry]) -> Option<String> {
742 entries
743 .iter()
744 .position(|entry| entry.reason.is_none())
745 .map(|index| format!("{key}[{index}]"))
746}
747
748#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
750#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
751#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
752#[non_exhaustive]
753pub struct Lint {
754 pub fail_on_warnings: bool,
756 #[serde(skip_serializing_if = "Option::is_none")]
758 pub max_level: Option<Level>,
759 #[serde(skip_serializing_if = "is_false")]
763 pub require_ignore_reasons: bool,
764}
765
766impl Lint {
767 #[must_use]
769 pub const fn with_fail_on_warnings(mut self, fail_on_warnings: bool) -> Self {
770 self.fail_on_warnings = fail_on_warnings;
771 self
772 }
773
774 #[must_use]
776 pub const fn with_max_level(mut self, max_level: Level) -> Self {
777 self.max_level = Some(max_level);
778 self
779 }
780
781 #[must_use]
783 pub const fn with_require_ignore_reasons(mut self, require: bool) -> Self {
784 self.require_ignore_reasons = require;
785 self
786 }
787}
788
789#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
792#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
793#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
794#[non_exhaustive]
795pub struct IgnoreEntry {
796 pub rules: Vec<String>,
798 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
800 pub packages: Selector,
801 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
803 pub dependencies: Selector,
804 #[serde(skip_serializing_if = "Selector::is_empty")]
808 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
809 pub files: Selector,
810 #[serde(skip_serializing_if = "Option::is_none")]
813 pub reason: Option<String>,
814}
815
816impl IgnoreEntry {
817 #[must_use]
819 pub fn with_rules<I, S>(mut self, rules: I) -> Self
820 where
821 I: IntoIterator<Item = S>,
822 S: Into<String>,
823 {
824 self.rules = rules.into_iter().map(Into::into).collect();
825 self
826 }
827
828 #[must_use]
830 pub fn with_packages(mut self, packages: Selector) -> Self {
831 self.packages = packages;
832 self
833 }
834
835 #[must_use]
837 pub fn with_dependencies(mut self, dependencies: Selector) -> Self {
838 self.dependencies = dependencies;
839 self
840 }
841
842 #[must_use]
844 pub fn with_files(mut self, files: Selector) -> Self {
845 self.files = files;
846 self
847 }
848
849 #[must_use]
851 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
852 self.reason = Some(reason.into());
853 self
854 }
855}
856
857#[derive(Debug, Clone, PartialEq, Eq, Hash)]
864#[non_exhaustive]
865pub struct IgnoreItem {
866 pub name: String,
868 pub reason: Option<String>,
871}
872
873impl IgnoreItem {
874 #[must_use]
876 pub fn new(name: impl Into<String>) -> Self {
877 Self {
878 name: name.into(),
879 reason: None,
880 }
881 }
882
883 #[must_use]
885 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
886 self.reason = Some(reason.into());
887 self
888 }
889}
890
891impl From<&str> for IgnoreItem {
892 fn from(name: &str) -> Self {
893 Self::new(name)
894 }
895}
896
897impl From<String> for IgnoreItem {
898 fn from(name: String) -> Self {
899 Self::new(name)
900 }
901}
902
903impl AsRef<str> for IgnoreItem {
904 fn as_ref(&self) -> &str {
905 &self.name
906 }
907}
908
909impl PartialEq<str> for IgnoreItem {
910 fn eq(&self, other: &str) -> bool {
911 self.name == other
912 }
913}
914
915impl PartialEq<&str> for IgnoreItem {
916 fn eq(&self, other: &&str) -> bool {
917 self.name == *other
918 }
919}
920
921impl PartialEq<String> for IgnoreItem {
922 fn eq(&self, other: &String) -> bool {
923 &self.name == other
924 }
925}
926
927impl Serialize for IgnoreItem {
928 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
929 use serde::ser::SerializeMap as _;
930
931 match &self.reason {
932 None => serializer.serialize_str(&self.name),
933 Some(reason) => {
934 let mut map = serializer.serialize_map(Some(2))?;
935 map.serialize_entry("name", &self.name)?;
936 map.serialize_entry("reason", reason)?;
937 map.end()
938 }
939 }
940 }
941}
942
943#[derive(Deserialize)]
945#[serde(deny_unknown_fields)]
946struct IgnoreItemTable {
947 name: String,
948 #[serde(default)]
949 reason: Option<String>,
950}
951
952impl<'de> Deserialize<'de> for IgnoreItem {
955 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
956 struct Visitor;
957
958 impl<'de> serde::de::Visitor<'de> for Visitor {
959 type Value = IgnoreItem;
960
961 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
962 f.write_str("a string, or a table of `name` and `reason`")
963 }
964
965 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
966 Ok(IgnoreItem::new(value))
967 }
968
969 fn visit_string<E: serde::de::Error>(self, value: String) -> Result<Self::Value, E> {
970 Ok(IgnoreItem::new(value))
971 }
972
973 fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
974 IgnoreItemTable::deserialize(serde::de::value::MapAccessDeserializer::new(map)).map(|table| {
975 IgnoreItem {
976 name: table.name,
977 reason: table.reason,
978 }
979 })
980 }
981 }
982
983 deserializer.deserialize_any(Visitor)
984 }
985}
986
987#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
989#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
990#[serde(rename_all = "kebab-case")]
991#[non_exhaustive]
992pub enum Prefer {
993 #[default]
995 Highest,
996 Lowest,
998}
999
1000#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1002#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1003#[serde(rename_all = "kebab-case")]
1004#[non_exhaustive]
1005pub enum OverrideReading {
1006 #[default]
1009 Skif,
1010 Syncpack,
1014}
1015
1016#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1018#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1019#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1020#[non_exhaustive]
1021pub struct Policy {
1022 pub prefer: Prefer,
1024 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1026 pub dependency_types: DependencyTypes,
1027 pub override_reading: OverrideReading,
1030}
1031
1032impl Policy {
1033 #[must_use]
1035 pub const fn with_prefer(mut self, prefer: Prefer) -> Self {
1036 self.prefer = prefer;
1037 self
1038 }
1039
1040 #[must_use]
1042 pub fn with_dependency_types(mut self, dependency_types: DependencyTypes) -> Self {
1043 self.dependency_types = dependency_types;
1044 self
1045 }
1046
1047 #[must_use]
1049 pub const fn with_override_reading(mut self, override_reading: OverrideReading) -> Self {
1050 self.override_reading = override_reading;
1051 self
1052 }
1053}
1054
1055#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1057#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1058#[serde(rename_all = "kebab-case")]
1059#[non_exhaustive]
1060pub enum VersionPolicy {
1061 Pinned,
1063 Banned,
1065 Ignored,
1068 SameRange,
1070 SameMinor,
1072 SnappedTo,
1074 #[default]
1076 PreferredSemver,
1077 SemverRangeOnly,
1079}
1080
1081#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1083#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1084#[serde(rename_all = "kebab-case")]
1085#[non_exhaustive]
1086pub enum GroupScope {
1087 #[default]
1089 Manifests,
1090 Lockfile,
1093}
1094
1095impl GroupScope {
1096 #[must_use]
1098 pub const fn is_manifests(&self) -> bool {
1099 matches!(self, Self::Manifests)
1100 }
1101}
1102
1103#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1106#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1107#[non_exhaustive]
1108pub struct VersionGroup {
1109 #[serde(skip_serializing_if = "Option::is_none")]
1111 pub label: Option<String>,
1112 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1114 pub dependencies: Selector,
1115 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1117 pub packages: Selector,
1118 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1120 pub dependency_types: DependencyTypes,
1121 pub specifier_types: Vec<String>,
1123 pub policy: VersionPolicy,
1125 #[serde(skip_serializing_if = "Option::is_none")]
1127 pub pin: Option<String>,
1128 pub snap_to: Vec<String>,
1130 #[serde(skip_serializing_if = "Option::is_none")]
1132 pub prefer: Option<Prefer>,
1133 #[serde(skip_serializing_if = "GroupScope::is_manifests")]
1135 pub scope: GroupScope,
1136}
1137
1138impl VersionGroup {
1139 #[must_use]
1141 pub const fn with_scope(mut self, scope: GroupScope) -> Self {
1142 self.scope = scope;
1143 self
1144 }
1145
1146 #[must_use]
1148 pub fn with_label(mut self, label: impl Into<String>) -> Self {
1149 self.label = Some(label.into());
1150 self
1151 }
1152
1153 #[must_use]
1155 pub fn with_dependencies(mut self, dependencies: Selector) -> Self {
1156 self.dependencies = dependencies;
1157 self
1158 }
1159
1160 #[must_use]
1162 pub fn with_packages(mut self, packages: Selector) -> Self {
1163 self.packages = packages;
1164 self
1165 }
1166
1167 #[must_use]
1169 pub fn with_dependency_types(mut self, dependency_types: DependencyTypes) -> Self {
1170 self.dependency_types = dependency_types;
1171 self
1172 }
1173
1174 #[must_use]
1176 pub fn with_specifier_types<I, S>(mut self, specifier_types: I) -> Self
1177 where
1178 I: IntoIterator<Item = S>,
1179 S: Into<String>,
1180 {
1181 self.specifier_types = specifier_types.into_iter().map(Into::into).collect();
1182 self
1183 }
1184
1185 #[must_use]
1187 pub const fn with_policy(mut self, policy: VersionPolicy) -> Self {
1188 self.policy = policy;
1189 self
1190 }
1191
1192 #[must_use]
1194 pub fn with_pin(mut self, pin: impl Into<String>) -> Self {
1195 self.pin = Some(pin.into());
1196 self
1197 }
1198
1199 #[must_use]
1201 pub fn with_snap_to<I, S>(mut self, snap_to: I) -> Self
1202 where
1203 I: IntoIterator<Item = S>,
1204 S: Into<String>,
1205 {
1206 self.snap_to = snap_to.into_iter().map(Into::into).collect();
1207 self
1208 }
1209
1210 #[must_use]
1212 pub const fn with_prefer(mut self, prefer: Prefer) -> Self {
1213 self.prefer = Some(prefer);
1214 self
1215 }
1216
1217 fn inconsistency(&self) -> Option<&'static str> {
1218 match self.policy {
1219 VersionPolicy::Pinned if self.pin.is_none() => return Some("policy `pinned` requires `pin`"),
1220 VersionPolicy::SnappedTo if self.snap_to.is_empty() => {
1221 return Some("policy `snapped-to` requires `snap-to`");
1222 }
1223 _ => {}
1224 }
1225 if self.pin.is_some() && self.policy != VersionPolicy::Pinned {
1226 return Some("`pin` is only valid with policy `pinned`");
1227 }
1228 if !self.snap_to.is_empty() && self.policy != VersionPolicy::SnappedTo {
1229 return Some("`snap-to` is only valid with policy `snapped-to`");
1230 }
1231 if self.prefer.is_some() && self.policy != VersionPolicy::PreferredSemver {
1232 return Some("`prefer` is only valid with policy `preferred-semver`");
1233 }
1234 if self.scope == GroupScope::Lockfile {
1235 if self.policy != VersionPolicy::Banned {
1236 return Some("`scope = \"lockfile\"` is only valid with policy `banned`");
1237 }
1238 if !self.dependency_types.is_empty() || !self.specifier_types.is_empty() {
1239 return Some(
1240 "`dependency-types` and `specifier-types` select manifest entries and are not valid with `scope = \"lockfile\"`",
1241 );
1242 }
1243 }
1244 None
1245 }
1246}
1247
1248#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1250#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1251#[non_exhaustive]
1252pub enum SemverRange {
1253 #[serde(rename = "")]
1255 Exact,
1256 #[serde(rename = "*")]
1258 Any,
1259 #[serde(rename = "^")]
1261 Caret,
1262 #[serde(rename = "~")]
1264 Tilde,
1265 #[serde(rename = ">")]
1267 Gt,
1268 #[serde(rename = ">=")]
1270 Gte,
1271 #[serde(rename = "<")]
1273 Lt,
1274 #[serde(rename = "<=")]
1276 Lte,
1277}
1278
1279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1282#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1283#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1284#[non_exhaustive]
1285pub struct SemverGroup {
1286 #[serde(default, skip_serializing_if = "Option::is_none")]
1288 pub label: Option<String>,
1289 #[serde(default)]
1291 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1292 pub dependencies: Selector,
1293 #[serde(default)]
1295 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1296 pub packages: Selector,
1297 #[serde(default)]
1299 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1300 pub dependency_types: DependencyTypes,
1301 #[serde(default)]
1303 pub specifier_types: Vec<String>,
1304 #[serde(default, skip_serializing_if = "Option::is_none")]
1306 pub range: Option<SemverRange>,
1307 #[serde(default, skip_serializing_if = "is_false")]
1310 pub ignored: bool,
1311}
1312
1313impl SemverGroup {
1314 #[must_use]
1316 pub fn new(range: SemverRange) -> Self {
1317 Self {
1318 label: None,
1319 dependencies: Selector::default(),
1320 packages: Selector::default(),
1321 dependency_types: DependencyTypes::default(),
1322 specifier_types: Vec::new(),
1323 range: Some(range),
1324 ignored: false,
1325 }
1326 }
1327
1328 #[must_use]
1330 pub fn ignored() -> Self {
1331 Self {
1332 range: None,
1333 ignored: true,
1334 ..Self::new(SemverRange::Exact)
1335 }
1336 }
1337
1338 fn inconsistency(&self) -> Option<&'static str> {
1339 match (self.range, self.ignored) {
1340 (None, false) => Some("set `range`, or `ignored = true` to exempt the dependencies from a range"),
1341 (Some(_), true) => Some("`range` and `ignored = true` are mutually exclusive"),
1342 _ => None,
1343 }
1344 }
1345
1346 #[must_use]
1348 pub fn with_label(mut self, label: impl Into<String>) -> Self {
1349 self.label = Some(label.into());
1350 self
1351 }
1352
1353 #[must_use]
1355 pub fn with_dependencies(mut self, dependencies: Selector) -> Self {
1356 self.dependencies = dependencies;
1357 self
1358 }
1359
1360 #[must_use]
1362 pub fn with_packages(mut self, packages: Selector) -> Self {
1363 self.packages = packages;
1364 self
1365 }
1366
1367 #[must_use]
1369 pub fn with_dependency_types(mut self, dependency_types: DependencyTypes) -> Self {
1370 self.dependency_types = dependency_types;
1371 self
1372 }
1373
1374 #[must_use]
1376 pub fn with_specifier_types<I, S>(mut self, specifier_types: I) -> Self
1377 where
1378 I: IntoIterator<Item = S>,
1379 S: Into<String>,
1380 {
1381 self.specifier_types = specifier_types.into_iter().map(Into::into).collect();
1382 self
1383 }
1384}
1385
1386#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1388#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1389#[serde(rename_all = "kebab-case")]
1390#[non_exhaustive]
1391pub enum UpdateTarget {
1392 #[default]
1394 Latest,
1395 Minor,
1397 Patch,
1399}
1400
1401impl UpdateTarget {
1402 #[must_use]
1405 pub const fn stricter(self, other: Self) -> Self {
1406 match (self, other) {
1407 (Self::Patch, _) | (_, Self::Patch) => Self::Patch,
1408 (Self::Minor, _) | (_, Self::Minor) => Self::Minor,
1409 _ => Self::Latest,
1410 }
1411 }
1412
1413 #[must_use]
1415 pub const fn name(self) -> &'static str {
1416 match self {
1417 Self::Latest => "latest",
1418 Self::Minor => "minor",
1419 Self::Patch => "patch",
1420 }
1421 }
1422}
1423
1424pub const DEFAULT_MAX_CONCURRENT_REQUESTS: usize = 12;
1426
1427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1429#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1430#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1431#[non_exhaustive]
1432pub struct Update {
1433 #[serde(skip_serializing_if = "Option::is_none")]
1436 pub minimum_release_age: Option<u64>,
1437 pub max_concurrent_requests: usize,
1439}
1440
1441impl Default for Update {
1442 fn default() -> Self {
1443 Self {
1444 minimum_release_age: None,
1445 max_concurrent_requests: DEFAULT_MAX_CONCURRENT_REQUESTS,
1446 }
1447 }
1448}
1449
1450impl Update {
1451 #[must_use]
1453 pub const fn with_minimum_release_age(mut self, minutes: Option<u64>) -> Self {
1454 self.minimum_release_age = minutes;
1455 self
1456 }
1457
1458 #[must_use]
1460 pub const fn with_max_concurrent_requests(mut self, requests: usize) -> Self {
1461 self.max_concurrent_requests = requests;
1462 self
1463 }
1464}
1465
1466#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1469#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1470#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1471#[non_exhaustive]
1472pub struct UpdateGroup {
1473 #[serde(default, skip_serializing_if = "Option::is_none")]
1475 pub label: Option<String>,
1476 #[serde(default)]
1478 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1479 pub dependencies: Selector,
1480 #[serde(default)]
1483 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1484 pub packages: Selector,
1485 #[serde(default)]
1488 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
1489 pub dependency_types: DependencyTypes,
1490 #[serde(default)]
1492 pub specifier_types: Vec<String>,
1493 #[serde(default, skip_serializing_if = "Option::is_none")]
1496 pub target: Option<UpdateTarget>,
1497 #[serde(default, skip_serializing_if = "is_false")]
1499 pub ignored: bool,
1500}
1501
1502impl UpdateGroup {
1503 #[must_use]
1505 pub fn new(target: UpdateTarget) -> Self {
1506 Self {
1507 label: None,
1508 dependencies: Selector::default(),
1509 packages: Selector::default(),
1510 dependency_types: DependencyTypes::default(),
1511 specifier_types: Vec::new(),
1512 target: Some(target),
1513 ignored: false,
1514 }
1515 }
1516
1517 #[must_use]
1519 pub fn ignored() -> Self {
1520 Self {
1521 target: None,
1522 ignored: true,
1523 ..Self::new(UpdateTarget::Latest)
1524 }
1525 }
1526
1527 fn inconsistency(&self) -> Option<&'static str> {
1528 match (self.target, self.ignored) {
1529 (None, false) => Some("set `target`, or `ignored = true` to leave the dependencies alone"),
1530 (Some(_), true) => Some("`target` and `ignored = true` are mutually exclusive"),
1531 _ => None,
1532 }
1533 }
1534
1535 #[must_use]
1537 pub fn with_label(mut self, label: impl Into<String>) -> Self {
1538 self.label = Some(label.into());
1539 self
1540 }
1541
1542 #[must_use]
1544 pub fn with_dependencies(mut self, dependencies: Selector) -> Self {
1545 self.dependencies = dependencies;
1546 self
1547 }
1548
1549 #[must_use]
1551 pub fn with_packages(mut self, packages: Selector) -> Self {
1552 self.packages = packages;
1553 self
1554 }
1555
1556 #[must_use]
1558 pub fn with_dependency_types(mut self, dependency_types: DependencyTypes) -> Self {
1559 self.dependency_types = dependency_types;
1560 self
1561 }
1562
1563 #[must_use]
1565 pub fn with_specifier_types<I, S>(mut self, specifier_types: I) -> Self
1566 where
1567 I: IntoIterator<Item = S>,
1568 S: Into<String>,
1569 {
1570 self.specifier_types = specifier_types.into_iter().map(Into::into).collect();
1571 self
1572 }
1573}
1574
1575#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1577#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1578#[non_exhaustive]
1579pub enum CustomStrategy {
1580 #[serde(rename = "name@version")]
1582 NameAtVersion,
1583 #[serde(rename = "version")]
1585 Version,
1586 #[serde(rename = "versions-by-name", alias = "versionsByName")]
1588 VersionsByName,
1589 #[serde(rename = "name~version")]
1592 NameTildeVersion,
1593}
1594
1595#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1598#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1599#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1600#[non_exhaustive]
1601pub struct CustomType {
1602 pub name: String,
1606 pub path: String,
1608 pub strategy: CustomStrategy,
1610 #[serde(default, skip_serializing_if = "Option::is_none")]
1612 pub name_path: Option<String>,
1613}
1614
1615impl CustomType {
1616 #[must_use]
1618 pub fn new(name: impl Into<String>, path: impl Into<String>, strategy: CustomStrategy) -> Self {
1619 Self {
1620 name: name.into(),
1621 path: path.into(),
1622 strategy,
1623 name_path: None,
1624 }
1625 }
1626
1627 #[must_use]
1629 pub fn with_name_path(mut self, name_path: impl Into<String>) -> Self {
1630 self.name_path = Some(name_path.into());
1631 self
1632 }
1633
1634 pub fn name_segments(&self) -> impl Iterator<Item = &str> {
1636 self.name_path.iter().flat_map(|path| path.split('.'))
1637 }
1638
1639 pub fn segments(&self) -> impl Iterator<Item = &str> {
1641 self.path.split('.')
1642 }
1643
1644 fn inconsistency(&self) -> Option<String> {
1645 if self.name.is_empty() || self.name.starts_with('!') {
1646 return Some("`name` must not be empty or start with `!`".to_owned());
1647 }
1648 if DependencyType::is_builtin_token(&self.name) {
1649 return Some(format!(
1650 "`name` \"{}\" is the name of a built-in dependency type",
1651 self.name
1652 ));
1653 }
1654 if self.segments().any(str::is_empty) {
1655 return Some(format!(
1656 "`path` \"{}\" must be a dotted path of non-empty keys",
1657 self.path
1658 ));
1659 }
1660 let takes_name_path = self.strategy == CustomStrategy::NameTildeVersion;
1661 match (takes_name_path, self.name_path.is_some()) {
1662 (true, false) => return Some("strategy `name~version` requires `name-path`".to_owned()),
1663 (false, true) => return Some("`name-path` is only valid with strategy `name~version`".to_owned()),
1664 _ => {}
1665 }
1666 if self.name_segments().any(str::is_empty) {
1667 return Some("`name-path` must be a dotted path of non-empty keys".to_owned());
1668 }
1669 None
1670 }
1671}
1672
1673#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1675#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1676#[serde(rename_all = "kebab-case")]
1677#[non_exhaustive]
1678pub enum CatalogMode {
1679 #[default]
1681 Off,
1682 Prefer,
1684 Enforce,
1686}
1687
1688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1690#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1691#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1692#[non_exhaustive]
1693pub struct Catalog {
1694 pub mode: CatalogMode,
1696 pub min_packages: usize,
1698 pub exclude: Vec<String>,
1700 pub report_unused_entries: bool,
1702 #[serde(skip_serializing_if = "Vec::is_empty")]
1704 pub extend_exclude: Vec<String>,
1705}
1706
1707impl Default for Catalog {
1708 fn default() -> Self {
1709 Self {
1710 mode: CatalogMode::Off,
1711 min_packages: 2,
1712 exclude: Vec::new(),
1713 report_unused_entries: false,
1714 extend_exclude: Vec::new(),
1715 }
1716 }
1717}
1718
1719impl Catalog {
1720 #[must_use]
1722 pub const fn with_mode(mut self, mode: CatalogMode) -> Self {
1723 self.mode = mode;
1724 self
1725 }
1726
1727 #[must_use]
1729 pub const fn with_min_packages(mut self, min_packages: usize) -> Self {
1730 self.min_packages = min_packages;
1731 self
1732 }
1733
1734 #[must_use]
1736 pub fn with_exclude<I, S>(mut self, exclude: I) -> Self
1737 where
1738 I: IntoIterator<Item = S>,
1739 S: Into<String>,
1740 {
1741 self.exclude = exclude.into_iter().map(Into::into).collect();
1742 self
1743 }
1744
1745 #[must_use]
1747 pub const fn with_report_unused_entries(mut self, report_unused_entries: bool) -> Self {
1748 self.report_unused_entries = report_unused_entries;
1749 self
1750 }
1751}
1752
1753#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
1755#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1756#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1757#[non_exhaustive]
1758pub struct WorkspaceProtocol {
1759 pub internal: bool,
1761 pub allow_local_paths: bool,
1763}
1764
1765impl WorkspaceProtocol {
1766 #[must_use]
1768 pub const fn with_internal(mut self, internal: bool) -> Self {
1769 self.internal = internal;
1770 self
1771 }
1772
1773 #[must_use]
1775 pub const fn with_allow_local_paths(mut self, allow_local_paths: bool) -> Self {
1776 self.allow_local_paths = allow_local_paths;
1777 self
1778 }
1779}
1780
1781#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1783#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1784#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
1785#[non_exhaustive]
1786pub struct Format {
1787 pub sort_first: Vec<String>,
1789 pub sort_az: Vec<String>,
1792 pub sort_exports: Vec<String>,
1795 pub sort_packages: bool,
1797 pub format_bugs: bool,
1799 pub format_repository: bool,
1801 #[serde(skip_serializing_if = "Option::is_none")]
1804 pub indent: Option<String>,
1805}
1806
1807fn strings(items: &[&str]) -> Vec<String> {
1808 items.iter().map(|item| (*item).to_owned()).collect()
1809}
1810
1811impl Default for Format {
1812 fn default() -> Self {
1813 Self {
1814 sort_first: strings(&["name", "description", "version", "author"]),
1815 sort_az: strings(&[
1816 "bin",
1817 "contributors",
1818 "dependencies",
1819 "devDependencies",
1820 "keywords",
1821 "peerDependencies",
1822 "resolutions",
1823 "scripts",
1824 ]),
1825 sort_exports: strings(&["types", "default"]),
1826 sort_packages: true,
1827 format_bugs: false,
1828 format_repository: false,
1829 indent: None,
1830 }
1831 }
1832}
1833
1834impl Format {
1835 #[must_use]
1837 pub fn with_sort_first<I: IntoIterator<Item = S>, S: Into<String>>(mut self, keys: I) -> Self {
1838 self.sort_first = keys.into_iter().map(Into::into).collect();
1839 self
1840 }
1841
1842 #[must_use]
1844 pub fn with_sort_az<I: IntoIterator<Item = S>, S: Into<String>>(mut self, keys: I) -> Self {
1845 self.sort_az = keys.into_iter().map(Into::into).collect();
1846 self
1847 }
1848
1849 #[must_use]
1851 pub fn with_sort_exports<I: IntoIterator<Item = S>, S: Into<String>>(mut self, keys: I) -> Self {
1852 self.sort_exports = keys.into_iter().map(Into::into).collect();
1853 self
1854 }
1855
1856 #[must_use]
1858 pub const fn with_sort_packages(mut self, sort: bool) -> Self {
1859 self.sort_packages = sort;
1860 self
1861 }
1862
1863 #[must_use]
1865 pub const fn with_format_bugs(mut self, format: bool) -> Self {
1866 self.format_bugs = format;
1867 self
1868 }
1869
1870 #[must_use]
1872 pub const fn with_format_repository(mut self, format: bool) -> Self {
1873 self.format_repository = format;
1874 self
1875 }
1876
1877 #[must_use]
1879 pub fn with_indent(mut self, indent: impl Into<String>) -> Self {
1880 self.indent = Some(indent.into());
1881 self
1882 }
1883
1884 #[must_use]
1886 pub fn indent_text(&self) -> Option<String> {
1887 self.indent.as_ref().map(|indent| indent.replace("\\t", "\t"))
1888 }
1889
1890 fn inconsistency(&self) -> Option<String> {
1891 let indent = self.indent_text()?;
1892 (indent.is_empty() || indent.chars().any(|c| c != ' ' && c != '\t')).then(|| {
1893 format!(
1894 "indent: {:?} is not a run of spaces or tabs",
1895 self.indent.as_deref().unwrap_or_default()
1896 )
1897 })
1898 }
1899}
1900
1901#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1903#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1904#[serde(rename_all = "kebab-case")]
1905#[non_exhaustive]
1906pub enum FamilyScope {
1907 #[default]
1909 Manifests,
1910 Importer,
1912 Lockfile,
1914}
1915
1916impl FamilyScope {
1917 #[must_use]
1919 pub const fn is_manifests(&self) -> bool {
1920 matches!(self, Self::Manifests)
1921 }
1922}
1923
1924#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1926#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1927#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1928#[non_exhaustive]
1929pub struct DependencyFamily {
1930 pub name: String,
1932 #[serde(default)]
1934 pub packages: Vec<String>,
1935 #[serde(default = "default_true")]
1937 pub enabled: bool,
1938 #[serde(default, skip_serializing_if = "FamilyScope::is_manifests")]
1941 pub scope: FamilyScope,
1942 #[serde(default, skip_serializing_if = "Option::is_none")]
1945 pub anchor: Option<String>,
1946}
1947
1948impl DependencyFamily {
1949 #[must_use]
1951 pub fn new<I, S>(name: impl Into<String>, packages: I) -> Self
1952 where
1953 I: IntoIterator<Item = S>,
1954 S: Into<String>,
1955 {
1956 Self {
1957 name: name.into(),
1958 packages: packages.into_iter().map(Into::into).collect(),
1959 enabled: true,
1960 scope: FamilyScope::Manifests,
1961 anchor: None,
1962 }
1963 }
1964
1965 #[must_use]
1967 pub const fn with_scope(mut self, scope: FamilyScope) -> Self {
1968 self.scope = scope;
1969 self
1970 }
1971
1972 #[must_use]
1974 pub fn with_anchor(mut self, anchor: impl Into<String>) -> Self {
1975 self.anchor = Some(anchor.into());
1976 self
1977 }
1978
1979 #[must_use]
1981 pub const fn with_enabled(mut self, enabled: bool) -> Self {
1982 self.enabled = enabled;
1983 self
1984 }
1985}
1986
1987const fn default_true() -> bool {
1988 true
1989}
1990
1991#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
1993#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1994#[serde(rename_all = "lowercase")]
1995#[non_exhaustive]
1996pub enum JsDiscovery {
1997 #[default]
2000 Auto,
2001 Git,
2006 Walk,
2008}
2009
2010#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
2012#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2013#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
2014#[non_exhaustive]
2015pub struct JsCycles {
2016 pub allow: Vec<Vec<String>>,
2019 pub dynamic_imports: bool,
2022}
2023
2024#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2026#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2027#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
2028#[non_exhaustive]
2029#[allow(
2030 clippy::struct_excessive_bools,
2031 reason = "each bool is an independent switch of the analysis"
2032)]
2033pub struct Js {
2034 pub enabled: bool,
2036 pub entry: Vec<String>,
2038 pub project: Vec<String>,
2040 pub ignore: Vec<String>,
2042 pub ignore_files: Vec<IgnoreItem>,
2045 pub ignore_dependencies: Vec<IgnoreItem>,
2047 pub ignore_binaries: Vec<IgnoreItem>,
2049 pub ignore_global_binaries: bool,
2053 pub ignore_unresolved: Vec<IgnoreItem>,
2056 pub ignore_workspaces: Vec<IgnoreItem>,
2058 pub ignore_exports_used_in_file: IgnoreExportsUsedInFile,
2060 pub ignore_members: Vec<IgnoreItem>,
2062 pub production: bool,
2064 pub isolate_workspaces: bool,
2071 pub strict: bool,
2076 #[serde(skip)]
2079 #[cfg_attr(feature = "schema", schemars(skip))]
2080 pub no_gitignore: bool,
2081 pub include_entry_exports: bool,
2083 #[serde(skip_serializing_if = "Option::is_none")]
2087 pub tsconfig: Option<String>,
2088 pub use_tsconfig_files: bool,
2091 pub include_fixtures: bool,
2094 pub use_public_hoisting: bool,
2099 pub discovery: JsDiscovery,
2105 pub cycles: JsCycles,
2107 pub workspaces: IndexMap<String, JsWorkspace>,
2109 pub plugins: IndexMap<String, PluginConfig>,
2111 #[serde(skip_serializing_if = "IndexMap::is_empty")]
2115 pub compilers: IndexMap<String, JsCompiler>,
2116 #[serde(skip_serializing_if = "Vec::is_empty")]
2118 pub extend_ignore: Vec<String>,
2119 #[serde(skip_serializing_if = "Vec::is_empty")]
2121 pub extend_ignore_dependencies: Vec<IgnoreItem>,
2122 #[serde(skip_serializing_if = "Vec::is_empty")]
2124 pub extend_ignore_binaries: Vec<IgnoreItem>,
2125 #[serde(skip_serializing_if = "Vec::is_empty")]
2127 pub extend_ignore_unresolved: Vec<IgnoreItem>,
2128}
2129
2130impl Js {
2131 pub(crate) fn ignore_lists(&self) -> [(&'static str, &[IgnoreItem]); 9] {
2133 [
2134 ("ignore-files", self.ignore_files.as_slice()),
2135 ("ignore-dependencies", self.ignore_dependencies.as_slice()),
2136 ("ignore-binaries", self.ignore_binaries.as_slice()),
2137 ("ignore-unresolved", self.ignore_unresolved.as_slice()),
2138 ("ignore-workspaces", self.ignore_workspaces.as_slice()),
2139 ("ignore-members", self.ignore_members.as_slice()),
2140 ("extend-ignore-dependencies", self.extend_ignore_dependencies.as_slice()),
2141 ("extend-ignore-binaries", self.extend_ignore_binaries.as_slice()),
2142 ("extend-ignore-unresolved", self.extend_ignore_unresolved.as_slice()),
2143 ]
2144 }
2145
2146 #[must_use]
2148 pub fn with_entry<I, S>(mut self, entry: I) -> Self
2149 where
2150 I: IntoIterator<Item = S>,
2151 S: Into<String>,
2152 {
2153 self.entry = entry.into_iter().map(Into::into).collect();
2154 self
2155 }
2156
2157 #[must_use]
2159 pub fn with_project<I, S>(mut self, project: I) -> Self
2160 where
2161 I: IntoIterator<Item = S>,
2162 S: Into<String>,
2163 {
2164 self.project = project.into_iter().map(Into::into).collect();
2165 self
2166 }
2167
2168 #[must_use]
2170 pub fn with_ignore_dependencies<I, S>(mut self, ignore_dependencies: I) -> Self
2171 where
2172 I: IntoIterator<Item = S>,
2173 S: Into<IgnoreItem>,
2174 {
2175 self.ignore_dependencies = ignore_dependencies.into_iter().map(Into::into).collect();
2176 self
2177 }
2178
2179 #[must_use]
2181 pub const fn with_enabled(mut self, enabled: bool) -> Self {
2182 self.enabled = enabled;
2183 self
2184 }
2185
2186 #[must_use]
2188 pub const fn with_production(mut self, production: bool) -> Self {
2189 self.production = production;
2190 self
2191 }
2192
2193 #[must_use]
2195 pub const fn with_isolate_workspaces(mut self, isolate: bool) -> Self {
2196 self.isolate_workspaces = isolate;
2197 self
2198 }
2199
2200 #[must_use]
2202 pub const fn with_strict(mut self, strict: bool) -> Self {
2203 self.strict = strict;
2204 self
2205 }
2206
2207 #[must_use]
2209 pub const fn production_mode(&self) -> bool {
2210 self.production || self.strict
2211 }
2212
2213 #[must_use]
2215 pub const fn isolated(&self) -> bool {
2216 self.isolate_workspaces || self.strict
2217 }
2218
2219 #[must_use]
2221 pub fn with_tsconfig(mut self, path: impl Into<String>) -> Self {
2222 self.tsconfig = Some(path.into());
2223 self
2224 }
2225
2226 #[must_use]
2229 pub const fn with_use_tsconfig_files(mut self, enabled: bool) -> Self {
2230 self.use_tsconfig_files = enabled;
2231 self
2232 }
2233
2234 #[must_use]
2237 pub const fn with_use_public_hoisting(mut self, enabled: bool) -> Self {
2238 self.use_public_hoisting = enabled;
2239 self
2240 }
2241
2242 #[must_use]
2244 pub fn with_plugin(mut self, name: impl Into<String>, plugin: PluginConfig) -> Self {
2245 self.plugins.insert(name.into(), plugin);
2246 self
2247 }
2248}
2249
2250impl Default for Js {
2251 fn default() -> Self {
2252 Self {
2253 enabled: true,
2254 entry: Vec::new(),
2255 project: Vec::new(),
2256 ignore: Vec::new(),
2257 ignore_files: Vec::new(),
2258 ignore_dependencies: Vec::new(),
2259 ignore_binaries: Vec::new(),
2260 ignore_global_binaries: true,
2261 ignore_unresolved: Vec::new(),
2262 ignore_workspaces: Vec::new(),
2263 ignore_exports_used_in_file: IgnoreExportsUsedInFile::default(),
2264 ignore_members: Vec::new(),
2265 production: false,
2266 isolate_workspaces: false,
2267 strict: false,
2268 no_gitignore: false,
2269 include_entry_exports: false,
2270 tsconfig: None,
2271 use_tsconfig_files: false,
2272 include_fixtures: false,
2273 use_public_hoisting: false,
2274 discovery: JsDiscovery::Auto,
2275 cycles: JsCycles::default(),
2276 workspaces: IndexMap::new(),
2277 plugins: IndexMap::new(),
2278 compilers: IndexMap::new(),
2279 extend_ignore: Vec::new(),
2280 extend_ignore_dependencies: Vec::new(),
2281 extend_ignore_binaries: Vec::new(),
2282 extend_ignore_unresolved: Vec::new(),
2283 }
2284 }
2285}
2286
2287#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
2289#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2290#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
2291#[non_exhaustive]
2292pub struct JsCompiler {
2293 #[serde(rename = "as", skip_serializing_if = "Option::is_none")]
2296 pub reader: Option<String>,
2297 #[serde(skip_serializing_if = "Option::is_none")]
2300 pub imports: Option<Vec<String>>,
2301}
2302
2303impl JsCompiler {
2304 pub const READERS: &'static [&'static str] = &[
2307 "astro", "cjs", "css", "cts", "js", "jsx", "less", "marko", "mdx", "mjs", "mts", "pcss", "sass", "scss",
2308 "styl", "stylus", "svelte", "ts", "tsrx", "tsx", "vue",
2309 ];
2310
2311 #[must_use]
2313 pub fn reading(extension: impl Into<String>) -> Self {
2314 Self {
2315 reader: Some(extension.into()),
2316 imports: None,
2317 }
2318 }
2319
2320 #[must_use]
2322 pub fn matching<I, S>(patterns: I) -> Self
2323 where
2324 I: IntoIterator<Item = S>,
2325 S: Into<String>,
2326 {
2327 Self {
2328 reader: None,
2329 imports: Some(patterns.into_iter().map(Into::into).collect()),
2330 }
2331 }
2332
2333 fn problem(&self, key: &str, extension: &str) -> Option<String> {
2335 let written = !extension.is_empty()
2336 && extension
2337 .bytes()
2338 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-' || byte == b'_');
2339 if !written {
2340 return Some(format!(
2341 "{key}: write the extension in lowercase without the leading dot (`html`, not `.html`)"
2342 ));
2343 }
2344 if Self::READERS.contains(&extension) {
2345 return Some(format!(
2346 "{key}: skif reads `.{extension}` files itself; remove the entry"
2347 ));
2348 }
2349 match (self.reader.as_deref(), self.imports.as_deref()) {
2350 (Some(reader), None) => (!Self::READERS.contains(&reader)).then(|| {
2351 let hint =
2352 closest(reader, Self::READERS).map_or_else(String::new, |s| format!("; did you mean \"{s}\"?"));
2353 format!(
2354 "{key}: unknown reader `{reader}`{hint} (expected one of {})",
2355 Self::READERS.join(", ")
2356 )
2357 }),
2358 (None, Some([])) => Some(format!("{key}.imports: list at least one regular expression")),
2359 (None, Some(patterns)) => patterns.iter().enumerate().find_map(|(index, pattern)| {
2360 pattern_problem(pattern).map(|message| format!("{key}.imports[{index}]: {message}"))
2361 }),
2362 _ => Some(format!("{key}: set exactly one of `as` and `imports`")),
2363 }
2364 }
2365}
2366
2367fn pattern_problem(pattern: &str) -> Option<String> {
2369 match regex::Regex::new(pattern) {
2370 Err(error) => Some(format!("not a valid regular expression: {error}")),
2371 Ok(regex) => {
2372 let groups = regex.captures_len().saturating_sub(1);
2373 (groups != 1)
2374 .then(|| format!("has {groups} capture groups; it needs exactly one, which captures the specifier"))
2375 }
2376 }
2377}
2378
2379#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
2384#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2385#[serde(untagged)]
2386#[non_exhaustive]
2387pub enum IgnoreExportsUsedInFile {
2388 All(bool),
2390 Kinds(ExportKinds),
2392}
2393
2394impl<'de> Deserialize<'de> for IgnoreExportsUsedInFile {
2395 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2396 struct Visitor;
2397
2398 impl<'de> serde::de::Visitor<'de> for Visitor {
2399 type Value = IgnoreExportsUsedInFile;
2400
2401 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2402 f.write_str("`true`, `false` or a table of export kinds")
2403 }
2404
2405 fn visit_bool<E: serde::de::Error>(self, value: bool) -> Result<Self::Value, E> {
2406 Ok(IgnoreExportsUsedInFile::All(value))
2407 }
2408
2409 fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<Self::Value, A::Error> {
2410 ExportKinds::deserialize(serde::de::value::MapAccessDeserializer::new(map))
2411 .map(IgnoreExportsUsedInFile::Kinds)
2412 }
2413 }
2414
2415 deserializer.deserialize_any(Visitor)
2416 }
2417}
2418
2419impl Default for IgnoreExportsUsedInFile {
2420 fn default() -> Self {
2421 Self::All(false)
2422 }
2423}
2424
2425#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
2427#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2428#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
2429#[non_exhaustive]
2430#[allow(clippy::struct_excessive_bools)]
2431pub struct ExportKinds {
2432 pub variable: bool,
2434 pub function: bool,
2436 pub class: bool,
2438 #[serde(rename = "enum")]
2440 pub enumeration: bool,
2441 pub namespace: bool,
2443 #[serde(rename = "type")]
2445 pub type_alias: bool,
2446 pub interface: bool,
2448 pub member: bool,
2451}
2452
2453#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
2455#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2456#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
2457#[non_exhaustive]
2458pub struct JsWorkspace {
2459 pub entry: Vec<String>,
2461 pub project: Vec<String>,
2463 pub ignore: Vec<String>,
2465 pub ignore_files: Vec<IgnoreItem>,
2467 pub ignore_dependencies: Vec<IgnoreItem>,
2469 pub ignore_binaries: Vec<IgnoreItem>,
2471 pub ignore_unresolved: Vec<IgnoreItem>,
2473 #[serde(skip_serializing_if = "Option::is_none")]
2475 pub include_entry_exports: Option<bool>,
2476 #[serde(skip_serializing_if = "Option::is_none")]
2478 pub ignore_exports_used_in_file: Option<IgnoreExportsUsedInFile>,
2479 pub ignore_members: Vec<IgnoreItem>,
2481 pub paths: IndexMap<String, Vec<String>>,
2483 pub plugins: IndexMap<String, PluginConfig>,
2485 #[serde(skip_serializing_if = "Vec::is_empty")]
2487 pub extend_ignore: Vec<String>,
2488 #[serde(skip_serializing_if = "Vec::is_empty")]
2490 pub extend_ignore_dependencies: Vec<IgnoreItem>,
2491 #[serde(skip_serializing_if = "Vec::is_empty")]
2493 pub extend_ignore_binaries: Vec<IgnoreItem>,
2494 #[serde(skip_serializing_if = "Vec::is_empty")]
2496 pub extend_ignore_unresolved: Vec<IgnoreItem>,
2497}
2498
2499impl JsWorkspace {
2500 pub(crate) fn ignore_lists(&self) -> [(&'static str, &[IgnoreItem]); 8] {
2502 [
2503 ("ignore-files", self.ignore_files.as_slice()),
2504 ("ignore-dependencies", self.ignore_dependencies.as_slice()),
2505 ("ignore-binaries", self.ignore_binaries.as_slice()),
2506 ("ignore-unresolved", self.ignore_unresolved.as_slice()),
2507 ("ignore-members", self.ignore_members.as_slice()),
2508 ("extend-ignore-dependencies", self.extend_ignore_dependencies.as_slice()),
2509 ("extend-ignore-binaries", self.extend_ignore_binaries.as_slice()),
2510 ("extend-ignore-unresolved", self.extend_ignore_unresolved.as_slice()),
2511 ]
2512 }
2513}
2514
2515#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2517#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2518#[serde(untagged)]
2519#[non_exhaustive]
2520pub enum PluginConfig {
2521 Enabled(bool),
2523 Options(PluginOptions),
2525}
2526
2527#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
2529#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2530#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
2531#[non_exhaustive]
2532pub struct PluginOptions {
2533 #[serde(skip_serializing_if = "Option::is_none")]
2535 pub enabled: Option<bool>,
2536 pub config: Vec<String>,
2538 pub entry: Vec<String>,
2540 pub project: Vec<String>,
2542}
2543
2544impl PluginOptions {
2545 #[must_use]
2547 pub fn new() -> Self {
2548 Self::default()
2549 }
2550
2551 #[must_use]
2553 pub const fn with_enabled(mut self, enabled: bool) -> Self {
2554 self.enabled = Some(enabled);
2555 self
2556 }
2557
2558 #[must_use]
2560 pub fn with_config<I, S>(mut self, config: I) -> Self
2561 where
2562 I: IntoIterator<Item = S>,
2563 S: Into<String>,
2564 {
2565 self.config = config.into_iter().map(Into::into).collect();
2566 self
2567 }
2568
2569 #[must_use]
2571 pub fn with_entry<I, S>(mut self, entry: I) -> Self
2572 where
2573 I: IntoIterator<Item = S>,
2574 S: Into<String>,
2575 {
2576 self.entry = entry.into_iter().map(Into::into).collect();
2577 self
2578 }
2579
2580 #[must_use]
2582 pub fn with_project<I, S>(mut self, project: I) -> Self
2583 where
2584 I: IntoIterator<Item = S>,
2585 S: Into<String>,
2586 {
2587 self.project = project.into_iter().map(Into::into).collect();
2588 self
2589 }
2590}
2591
2592#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
2597#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2598#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
2599#[non_exhaustive]
2600pub struct PackageJs {
2601 #[serde(skip_serializing_if = "Option::is_none")]
2603 pub entry: Option<Vec<String>>,
2604 #[serde(skip_serializing_if = "Option::is_none")]
2606 pub project: Option<Vec<String>>,
2607 #[serde(skip_serializing_if = "Option::is_none")]
2609 pub ignore: Option<Vec<String>>,
2610 #[serde(skip_serializing_if = "Option::is_none")]
2612 pub ignore_dependencies: Option<Vec<IgnoreItem>>,
2613 #[serde(skip_serializing_if = "Option::is_none")]
2615 pub ignore_binaries: Option<Vec<IgnoreItem>>,
2616 #[serde(skip_serializing_if = "Vec::is_empty")]
2618 pub extend_ignore: Vec<String>,
2619 #[serde(skip_serializing_if = "Vec::is_empty")]
2621 pub extend_ignore_dependencies: Vec<IgnoreItem>,
2622 #[serde(skip_serializing_if = "Vec::is_empty")]
2624 pub extend_ignore_binaries: Vec<IgnoreItem>,
2625 #[serde(skip_serializing_if = "Option::is_none")]
2627 pub ignore_unresolved: Option<Vec<IgnoreItem>>,
2628 #[serde(skip_serializing_if = "Vec::is_empty")]
2630 pub extend_ignore_unresolved: Vec<IgnoreItem>,
2631 #[serde(skip_serializing_if = "IndexMap::is_empty")]
2633 pub plugins: IndexMap<String, PluginConfig>,
2634}
2635
2636impl PackageJs {
2637 pub(crate) fn ignore_lists(&self) -> [(&'static str, &[IgnoreItem]); 6] {
2639 [
2640 (
2641 "ignore-dependencies",
2642 self.ignore_dependencies.as_deref().unwrap_or_default(),
2643 ),
2644 ("ignore-binaries", self.ignore_binaries.as_deref().unwrap_or_default()),
2645 (
2646 "ignore-unresolved",
2647 self.ignore_unresolved.as_deref().unwrap_or_default(),
2648 ),
2649 ("extend-ignore-dependencies", self.extend_ignore_dependencies.as_slice()),
2650 ("extend-ignore-binaries", self.extend_ignore_binaries.as_slice()),
2651 ("extend-ignore-unresolved", self.extend_ignore_unresolved.as_slice()),
2652 ]
2653 }
2654
2655 #[must_use]
2657 pub fn is_empty(&self) -> bool {
2658 self == &Self::default()
2659 }
2660}
2661
2662#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
2673#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2674#[serde(rename_all = "kebab-case", deny_unknown_fields, default)]
2675#[non_exhaustive]
2676pub struct Override {
2677 #[serde(skip_serializing_if = "Selector::is_empty")]
2679 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
2680 pub packages: Selector,
2681 #[serde(skip_serializing_if = "Selector::is_empty")]
2683 #[cfg_attr(feature = "schema", schemars(with = "Vec<String>"))]
2684 pub files: Selector,
2685 #[serde(skip_serializing_if = "Option::is_none")]
2687 pub max_level: Option<Level>,
2688 #[serde(with = "rules_map", skip_serializing_if = "IndexMap::is_empty")]
2690 #[cfg_attr(feature = "schema", schemars(with = "IndexMap<String, RuleConfigSchema>"))]
2691 pub rules: IndexMap<String, RuleConfig>,
2692 #[serde(skip_serializing_if = "Vec::is_empty")]
2694 pub ignore: Vec<IgnoreEntry>,
2695 #[serde(skip_serializing_if = "PackageJs::is_empty")]
2697 pub js: PackageJs,
2698 #[serde(skip)]
2700 pub scope: Option<OverrideScope>,
2701}
2702
2703impl Override {
2704 pub(crate) fn inconsistency(&self) -> Option<&'static str> {
2705 if self.packages.is_empty() && self.files.is_empty() {
2706 return Some(
2707 "set at least one of `packages` and `files`; an override without a selector would apply everywhere",
2708 );
2709 }
2710 if !self.js.is_empty() && (self.packages.is_empty() || !self.files.is_empty()) {
2711 return Some("`js` needs `packages` and cannot be combined with `files`");
2712 }
2713 if self.rules.values().any(|rule| !rule.options.is_empty()) {
2714 return Some("rule options are root-scoped; an override can only set rule levels");
2715 }
2716 None
2717 }
2718
2719 #[must_use]
2725 pub fn applies_to(&self, package: Option<(&str, &Path)>, file: Option<&Path>) -> bool {
2726 self.scope.as_ref().is_some_and(|scope| scope.applies_to(package, file))
2727 }
2728
2729 #[must_use]
2732 pub fn applies_to_slashed(&self, package: Option<(&str, &str)>, file: Option<&str>) -> bool {
2733 self.scope
2734 .as_ref()
2735 .is_some_and(|scope| scope.applies_to_slashed(package, file))
2736 }
2737
2738 #[must_use]
2742 pub fn literal_packages(&self) -> Option<impl Iterator<Item = &str>> {
2743 self.scope.as_ref().and_then(OverrideScope::literal_packages)
2744 }
2745}
2746
2747#[derive(Debug, Clone, PartialEq)]
2749#[non_exhaustive]
2750pub struct OverrideScope {
2751 pub declared_in: PathBuf,
2753 pub source: usize,
2755 has_packages: bool,
2756 has_files: bool,
2757 packages: Selector,
2758 files: Selector,
2759 unanchored: Vec<String>,
2760}
2761
2762impl OverrideScope {
2763 pub fn new(scoped: &Override, declared_in: &Path, source: usize, root: &Path) -> Result<Self, ConfigError> {
2768 Ok(Self {
2769 declared_in: declared_in.to_path_buf(),
2770 source,
2771 has_packages: !scoped.packages.is_empty(),
2772 has_files: !scoped.files.is_empty(),
2773 packages: anchor_patterns(scoped.packages.patterns(), declared_in, root, false)?,
2774 files: anchor_patterns(scoped.files.patterns(), declared_in, root, true)?,
2775 unanchored: unanchored(scoped.packages.patterns(), false)
2776 .into_iter()
2777 .chain(unanchored(scoped.files.patterns(), true))
2778 .collect(),
2779 })
2780 }
2781
2782 #[must_use]
2785 pub fn unanchored(&self) -> &[String] {
2786 &self.unanchored
2787 }
2788
2789 fn literal_packages(&self) -> Option<impl Iterator<Item = &str>> {
2790 if !self.has_packages || self.has_files {
2791 return None;
2792 }
2793 self.packages.only_literals()
2794 }
2795
2796 fn applies_to(&self, package: Option<(&str, &Path)>, file: Option<&Path>) -> bool {
2797 let package = package.map(|(name, dir)| (name, skif_core::slash(dir)));
2798 let file = file.map(skif_core::slash);
2799 self.applies_to_slashed(
2800 package.as_ref().map(|(name, dir)| (*name, dir.as_str())),
2801 file.as_deref(),
2802 )
2803 }
2804
2805 fn applies_to_slashed(&self, package: Option<(&str, &str)>, file: Option<&str>) -> bool {
2806 let package_ok =
2807 !self.has_packages || package.is_some_and(|(name, dir)| self.packages.matches_any([name, dir], true));
2808 let file_ok = !self.has_files || file.is_some_and(|file| self.files.matches(file, false));
2809 package_ok && file_ok
2810 }
2811}