Skip to main content

deps_core/lsp_helpers/
formatter.rs

1//! Ecosystem-specific formatting and comparison logic, split into concern-scoped traits.
2//!
3//! [`EcosystemFormatter`] is kept as a single object-safe marker bound so every existing
4//! `&dyn EcosystemFormatter` call site is untouched; it is automatically implemented for any
5//! type implementing all seven concern traits below via a blanket impl, so implementors never
6//! write `impl EcosystemFormatter for X` themselves. The seven traits are independent siblings
7//! — none of them has a default method that calls a method living in a different trait — so
8//! implementing a subset of them (e.g. in a test mock that only needs [`PackageRendering`]) is
9//! always sufficient for calling that subset's methods directly, without pulling in the rest.
10
11use tower_lsp_server::ls_types::Position;
12
13use super::{RequirementMatcher, RequirementStatus, is_same_major_minor, position_in_range};
14use crate::{ConcreteVersion, Dependency, InvalidPackageName, PackageName, VersionReq};
15
16/// Ecosystem-specific package name normalization and validation.
17///
18/// Implementors guarantee that [`normalize_package_name`](Self::normalize_package_name)
19/// produces a stable lookup key for the same logical package regardless of how its name is
20/// spelled in a manifest, and that [`validate_package_name`](Self::validate_package_name) is a
21/// diagnostic lint only — never a construction-time gate. Callers may assume both methods are
22/// cheap, side-effect-free, and safe to call on unvalidated, manifest-sourced input.
23pub trait PackageNaming: Send + Sync {
24    /// Normalize package name for lookup (default: identity).
25    fn normalize_package_name(&self, name: &PackageName) -> String {
26        name.to_string()
27    }
28
29    /// Lints `name` against ecosystem-specific naming rules.
30    ///
31    /// Default: permissive, always `Ok(())`. This is a diagnostic lint, not a
32    /// construction-time gate — [`PackageName::new`](crate::PackageName::new)
33    /// stays infallible regardless of what this returns. Override only to warn
34    /// on names an ecosystem's own tooling would never accept; err on the side
35    /// of accepting anything ambiguous, since a false positive here is a
36    /// warning on a manifest the user's actual package manager treats as fine.
37    ///
38    /// # Errors
39    ///
40    /// Returns [`InvalidPackageName`] carrying the reason `name` fails this
41    /// ecosystem's naming rules. The default implementation never errs.
42    ///
43    /// # Examples
44    ///
45    /// ```
46    /// use deps_core::lsp_helpers::PackageNaming;
47    ///
48    /// struct PermissiveFormatter;
49    ///
50    /// impl PackageNaming for PermissiveFormatter {}
51    ///
52    /// // The default is permissive: any name, including one that would fail an
53    /// // ecosystem-specific override, is accepted.
54    /// assert!(PermissiveFormatter.validate_package_name("../not/a/real/rule").is_ok());
55    /// ```
56    fn validate_package_name(&self, _name: &str) -> Result<(), InvalidPackageName> {
57        Ok(())
58    }
59}
60
61/// How a package/version renders into manifest text edits and hover content.
62///
63/// Implementors guarantee that [`format_version_for_text_edit`](Self::format_version_for_text_edit)
64/// and [`package_url`](Self::package_url) — the trait's only two required methods — produce
65/// text safe to embed directly in a manifest or hover response for any version/name that has
66/// already passed the workspace's shared safety gates
67/// ([`crate::is_safe_version_string`], [`crate::is_safe_package_name`]). Callers may assume the
68/// replacement-preserving methods ([`format_version_replacing`](Self::format_version_replacing),
69/// [`format_version_replacing_for`](Self::format_version_replacing_for)) never change a
70/// requirement's semantics unless the ecosystem has explicitly opted in to that transformation.
71pub trait PackageRendering: Send + Sync {
72    /// Format version string for code action text edit.
73    fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String;
74
75    /// Format `version` as a replacement for the existing requirement text
76    /// `current`, preserving `current`'s operator/pin style where the
77    /// ecosystem supports more than one.
78    ///
79    /// Default: ignores `current`, delegating to
80    /// [`format_version_for_text_edit`](Self::format_version_for_text_edit).
81    /// Override when a bare `format_version_for_text_edit` replacement would
82    /// silently change the requirement's semantics — e.g. PyPI's `==1.0.1`
83    /// pin becoming `>=1.0.1,<2` on "update version" would defeat the point
84    /// of pinning.
85    fn format_version_replacing(&self, version: &ConcreteVersion, _current: &str) -> String {
86        self.format_version_for_text_edit(version)
87    }
88
89    /// Like [`format_version_replacing`](Self::format_version_replacing), but also
90    /// carries the dependency identity `version`/`current` apply to.
91    ///
92    /// Default: ignores `dep`, delegating to
93    /// [`format_version_replacing`](Self::format_version_replacing). Override when the
94    /// replacement text cannot be derived from `version`/`current` alone — e.g.
95    /// `deps-github-actions`'s SHA-pinned `uses: owner/repo@<sha> # vX.Y.Z` form, where
96    /// the new SHA for a given tag is looked up per `dep.name()` (a tag's commit SHA is
97    /// per-repository, unknowable from the tag string alone) in a registry-populated
98    /// index the formatter holds a shared handle to.
99    ///
100    /// Every shared call site that builds a version-update edit (the vulnerability and
101    /// unsatisfiable-requirement quickfixes, the REFACTOR-loop "update to X" actions, and
102    /// the "Update N outdated dependencies" code lens) already has `dep` in scope and
103    /// calls this method instead of [`format_version_replacing`](Self::format_version_replacing)
104    /// directly, so an override here is picked up on every edit path at once.
105    fn format_version_replacing_for(
106        &self,
107        _dep: &dyn Dependency,
108        version: &ConcreteVersion,
109        current: &str,
110    ) -> String {
111        self.format_version_replacing(version, current)
112    }
113
114    /// Get package URL for hover markdown.
115    fn package_url(&self, name: &PackageName) -> String;
116
117    /// Whether hover should omit [`Self::package_url`]'s heading link for a dependency
118    /// resolved against `source`.
119    ///
120    /// [`Self::package_url`] always names the ecosystem's *default* public registry (e.g.
121    /// crates.io) — correct for a plain [`DependencySource::Registry`](crate::parser::DependencySource::Registry)
122    /// dependency, but wrong for one resolved against a different registry entirely (e.g.
123    /// `deps-cargo`'s resolved `AlternateRegistry`): once live version data from that other
124    /// registry renders alongside the link, an unrelated crates.io link reads as
125    /// confirmation the link is real, which is worse than showing no link at all.
126    ///
127    /// Default `false` — every ecosystem with only one registry concept keeps its existing
128    /// hover heading unchanged; only `deps-cargo`'s `CargoFormatter` overrides this.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use deps_core::lsp_helpers::PackageRendering;
134    /// use deps_core::parser::DependencySource;
135    /// use deps_core::{ConcreteVersion, PackageName};
136    ///
137    /// struct DefaultFormatter;
138    /// impl PackageRendering for DefaultFormatter {
139    ///     fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
140    ///         version.to_string()
141    ///     }
142    ///     fn package_url(&self, name: &PackageName) -> String {
143    ///         name.to_string()
144    ///     }
145    /// }
146    ///
147    /// assert!(!DefaultFormatter.suppress_package_url(&DependencySource::Registry));
148    /// ```
149    fn suppress_package_url(&self, source: &crate::parser::DependencySource) -> bool {
150        let _ = source;
151        false
152    }
153
154    /// Detect if cursor position is on a dependency for code actions.
155    fn is_position_on_dependency(&self, dep: &dyn Dependency, position: Position) -> bool {
156        dep.version_range()
157            .is_some_and(|r| position_in_range(position, r))
158    }
159}
160
161/// Requirement parsing, matching, and up-to-date status.
162///
163/// Implementors guarantee every method here is a pure function of its arguments — no network
164/// or filesystem access — since these run on the hot hover/diagnostic path. The default
165/// [`requirement_status`](Self::requirement_status) maps
166/// [`requirement_is_unresolved`](Self::requirement_is_unresolved) to its `Unresolved` variant
167/// and otherwise defers to [`is_requirement_up_to_date`](Self::is_requirement_up_to_date) — but
168/// an override of one without the other is not a contract violation: an ecosystem whose
169/// requirement syntax can be unresolved (Maven, Gradle, NuGet, `deps-github-actions`) overrides
170/// `requirement_is_unresolved` precisely so `requirement_status` can distinguish "not yet
171/// decidable" from "decided outdated", a distinction the boolean method has no variant for.
172/// Callers needing that distinction use `requirement_status`, not the boolean method.
173pub trait RequirementResolution: Send + Sync {
174    /// Check if a version satisfies a requirement string.
175    ///
176    /// General constraint check (e.g. for completion/candidate filtering) — not the
177    /// "is this dependency up to date" hook. That is `is_requirement_up_to_date` below,
178    /// which has its own default and its own override points; an ecosystem whose bare
179    /// requirement is a floor rather than an auto-following range (see `deps-nuget`)
180    /// overrides that method, not this one.
181    fn version_satisfies_requirement(&self, version: &ConcreteVersion, requirement: &str) -> bool {
182        let version = version.as_str();
183        // Handle caret (^) - allows changes that don't modify left-most non-zero
184        // ^2.0 allows 2.x.x, ^0.2 allows 0.2.x, ^0.0.3 allows only 0.0.3
185        if let Some(req) = requirement.strip_prefix('^') {
186            let req_parts: Vec<&str> = req.split('.').collect();
187            let ver_parts: Vec<&str> = version.split('.').collect();
188
189            // Must have same major version
190            if req_parts.first() != ver_parts.first() {
191                return false;
192            }
193
194            // For ^X.Y where X > 0, any X.*.* is allowed
195            if req_parts.first().is_some_and(|m| *m != "0") {
196                return true;
197            }
198
199            // For ^0.Y, must have same minor
200            if req_parts.len() >= 2 && ver_parts.len() >= 2 {
201                return req_parts[1] == ver_parts[1];
202            }
203
204            return true;
205        }
206
207        // Handle tilde (~) - allows patch-level changes
208        // ~2.0 allows 2.0.x, ~2.0.1 allows 2.0.x where x >= 1
209        if let Some(req) = requirement.strip_prefix('~') {
210            return is_same_major_minor(req, version);
211        }
212
213        // Plain version or partial version
214        let req_parts: Vec<&str> = requirement.split('.').collect();
215        let is_partial_version = req_parts.len() <= 2;
216
217        version == requirement
218            || (is_partial_version && is_same_major_minor(requirement, version))
219            || (is_partial_version && version.starts_with(requirement))
220    }
221
222    /// Whether an unresolved dependency (no lock-file version) should be reported as
223    /// up to date against `latest`, given its declared `requirement`.
224    ///
225    /// Default: `latest` satisfies `requirement` — correct for range-based ecosystems
226    /// (Cargo's `^1.2`, npm's `~1.2`, ...) where the declared requirement already
227    /// expresses forward compatibility, so a `latest` it accepts is not "newer" in any
228    /// actionable sense. Ecosystems where a bare requirement is a minimum floor rather
229    /// than an auto-following range (NuGet's bare `Version="1.0.0"`) must override this,
230    /// since "does the floor accept `latest`" and "is the pin already `latest`" are
231    /// different questions there.
232    fn is_requirement_up_to_date(
233        &self,
234        requirement: &VersionReq,
235        latest: &ConcreteVersion,
236    ) -> bool {
237        self.version_satisfies_requirement(latest, requirement.as_str())
238    }
239
240    /// Whether `requirement` could not be resolved to a concrete version constraint (e.g. an
241    /// unexpanded property/variable placeholder rather than a real version or range).
242    ///
243    /// Default: always resolvable. Ecosystems whose requirement syntax can contain
244    /// unresolved placeholders (Maven's `${property}`, Gradle's `$var`/`${var}`) override
245    /// this single predicate; both `version_satisfies_requirement`'s "treat as satisfied"
246    /// short-circuit and `requirement_status`'s `Unresolved` variant are derived from it, so
247    /// the two can't drift out of sync with each other.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use deps_core::lsp_helpers::RequirementResolution;
253    /// use deps_core::VersionReq;
254    ///
255    /// struct DefaultFormatter;
256    /// impl RequirementResolution for DefaultFormatter {}
257    ///
258    /// assert!(!DefaultFormatter.requirement_is_unresolved(&VersionReq::new("^1.2")));
259    /// ```
260    fn requirement_is_unresolved(&self, _requirement: &VersionReq) -> bool {
261        false
262    }
263
264    /// Tri-state variant of `is_requirement_up_to_date` that distinguishes "confirmed up to
265    /// date" from "could not be resolved, so we don't know."
266    ///
267    /// Default: `Unresolved` when `requirement_is_unresolved` says so, otherwise maps the
268    /// boolean result of `is_requirement_up_to_date` to `UpToDate`/`Outdated`. Callers
269    /// needing the distinction — inlay hints, in particular — use this instead of
270    /// `is_requirement_up_to_date` so they can tell "verified up to date" apart from
271    /// "resolution failed."
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// use deps_core::lsp_helpers::{RequirementResolution, RequirementStatus};
277    /// use deps_core::{ConcreteVersion, VersionReq};
278    ///
279    /// struct DefaultFormatter;
280    /// impl RequirementResolution for DefaultFormatter {}
281    ///
282    /// assert_eq!(
283    ///     DefaultFormatter.requirement_status(&VersionReq::new("^1.2"), &ConcreteVersion::new("1.5.0")),
284    ///     RequirementStatus::UpToDate
285    /// );
286    /// assert_eq!(
287    ///     DefaultFormatter.requirement_status(&VersionReq::new("^1.2"), &ConcreteVersion::new("2.0.0")),
288    ///     RequirementStatus::Outdated
289    /// );
290    /// ```
291    fn requirement_status(
292        &self,
293        requirement: &VersionReq,
294        latest: &ConcreteVersion,
295    ) -> RequirementStatus {
296        if self.requirement_is_unresolved(requirement) {
297            return RequirementStatus::Unresolved;
298        }
299        if self.is_requirement_up_to_date(requirement, latest) {
300            RequirementStatus::UpToDate
301        } else {
302            RequirementStatus::Outdated
303        }
304    }
305
306    /// Like [`requirement_status`](Self::requirement_status), but also hands the ecosystem
307    /// the dependency itself — for an ecosystem whose requirement *text* alone is ambiguous
308    /// between two shapes with different resolution rules, and which already computed the
309    /// disambiguating classification once, at parse time, onto the dependency (`deps-gitlab-ci`'s
310    /// `PinStyle`, #466 review M-c: a bare `"1.2"` is `Partial` under its `component:` pin
311    /// grammar but `Branch` under its simpler `project:` ref grammar — indistinguishable from
312    /// the text alone).
313    ///
314    /// Default: forwards to [`requirement_status`](Self::requirement_status), ignoring `dep`
315    /// — every other ecosystem's requirement text alone is unambiguous, so this is a no-op
316    /// for them. Callers that already have `dep` in hand (the diagnostic pipeline's outdated
317    /// rule) call this instead of `requirement_status` directly, mirroring
318    /// `Registry::select_latest_matching_with_context`'s identical additive-default pattern.
319    fn requirement_status_for(
320        &self,
321        dep: &dyn Dependency,
322        requirement: &VersionReq,
323        latest: &ConcreteVersion,
324    ) -> RequirementStatus {
325        let _ = dep;
326        self.requirement_status(requirement, latest)
327    }
328
329    /// Compiles `requirement` into a matcher for precise membership testing against a list
330    /// of candidate version strings, or `None` when this ecosystem cannot parse or cannot
331    /// model this requirement form — in which case no unsatisfiable-requirement diagnostic
332    /// is produced for it.
333    ///
334    /// Distinct from `version_satisfies_requirement`, which answers the looser "treat as up
335    /// to date" question and is deliberately permissive (see that method's docs). This one
336    /// gates a WARNING diagnostic claiming "no published version satisfies this
337    /// requirement", so it must never guess: an ecosystem that has not opted in by
338    /// overriding this method emits no such diagnostic at all, rather than one derived from
339    /// a loose heuristic.
340    ///
341    /// `None` has two distinct causes, both correct to suppress the diagnostic for: the
342    /// requirement string fails to parse under this ecosystem's own comparator (`deps-cargo`,
343    /// `deps-npm`, `deps-pypi`, `deps-swift` — `.ok()` on a fallible parse), or the
344    /// requirement parses fine but names a version-space region the fetched `available` list
345    /// structurally cannot contain regardless — a Go pseudo-version, a Composer
346    /// dev-branch/`@dev` flag, a RubyGems exact pin indistinguishable from one that matches
347    /// only a yanked release, a malformed Maven/Gradle/NuGet range. Scanning either case would
348    /// always decide `Some(false)` for every candidate, producing a false "no published
349    /// version satisfies" verdict instead of correctly suppressing the check. Implementors of
350    /// the second (predicate-guard) shape should use
351    /// [`crate::lsp_helpers::compile_requirement_unless`], which
352    /// centralizes this contract instead of re-deriving it per ecosystem. `deps-dart` is the
353    /// only ecosystem with neither cause: every requirement string is a valid Dart constraint
354    /// by construction, so its override is always `Some`.
355    ///
356    /// Default: `None` — an ecosystem that has not opted in emits no unsatisfiable-requirement
357    /// diagnostics.
358    ///
359    /// # Examples
360    ///
361    /// ```
362    /// use deps_core::lsp_helpers::RequirementResolution;
363    /// use deps_core::VersionReq;
364    ///
365    /// struct DefaultFormatter;
366    /// impl RequirementResolution for DefaultFormatter {}
367    ///
368    /// assert!(
369    ///     DefaultFormatter
370    ///         .compile_requirement(&VersionReq::new("^1.2"))
371    ///         .is_none()
372    /// );
373    /// ```
374    fn compile_requirement(
375        &self,
376        _requirement: &VersionReq,
377    ) -> Option<Box<dyn RequirementMatcher>> {
378        None
379    }
380
381    /// Whether this ecosystem's registry can silently omit a *published* version from
382    /// `available` in a way indistinguishable from "never published" — and, if so, whether
383    /// `requirement` names a version-space region that specific omission could explain, given
384    /// the versions actually observed in `available`.
385    ///
386    /// Called by [`crate::lsp_helpers::requirement_is_unsatisfiable`] before compiling `requirement`; returning
387    /// `true` suppresses the "no published version satisfies this requirement" diagnostic for
388    /// this dependency, the same as [`Self::compile_requirement`] returning `None` — but,
389    /// unlike that method, this one sees `available` and can therefore narrow the suppression
390    /// instead of disabling it for every requirement of a given shape.
391    ///
392    /// Default `false` — no ecosystem has this problem unless it opts in. `deps-bundler`
393    /// overrides it (see `BundlerFormatter::requirement_is_undecidable_given_available` and
394    /// its helper for the RubyGems-specific rationale and heuristic).
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// use deps_core::lsp_helpers::RequirementResolution;
400    /// use deps_core::{ConcreteVersion, VersionReq};
401    ///
402    /// struct DefaultFormatter;
403    /// impl RequirementResolution for DefaultFormatter {}
404    ///
405    /// assert!(!DefaultFormatter.requirement_is_undecidable_given_available(
406    ///     &VersionReq::new("1.6.13"),
407    ///     &[ConcreteVersion::new("1.6.9"), ConcreteVersion::new("1.6.14")],
408    /// ));
409    /// ```
410    fn requirement_is_undecidable_given_available(
411        &self,
412        _requirement: &VersionReq,
413        _available: &[ConcreteVersion],
414    ) -> bool {
415        false
416    }
417
418    /// Whether `dep`'s manifest version-requirement line is itself the exact
419    /// version already selected — never a range.
420    ///
421    /// True only for a Go `require`-directive dependency: `go.mod`'s
422    /// `require` line already holds the module version selected by Go's
423    /// MVS, unlike Cargo/npm where the manifest holds a range and the lock
424    /// file holds the pin. When true, hover and inlay hints prefer
425    /// [`Dependency::version_requirement`] over the lock-file-derived entry
426    /// in [`crate::lsp_helpers::VersionData::resolved`], because `go.sum` is a checksum ledger
427    /// that `go get`/`go build` only ever append to (only `go mod tidy`
428    /// prunes it) — a stale, no-longer-selected higher version can remain
429    /// recorded there after a downgrade and, since go.sum is written sorted
430    /// ascending by semver, always sorts last and wins naive
431    /// last-occurrence-wins parsing (overridden in `deps-go`; see `#235`).
432    ///
433    /// Takes `dep` (precedent: [`OsvNaming::osv_package_name`]) because Go's
434    /// `exclude`/`replace` directives are also surfaced as dependencies
435    /// whose `version_requirement()` is *not* an in-use version (the
436    /// excluded version, or the replaced-from version) — the `deps-go`
437    /// override inspects the directive kind and returns `true` only for
438    /// `require`.
439    fn manifest_requirement_is_resolved_version(&self, dep: &dyn Dependency) -> bool {
440        let _ = dep;
441        false
442    }
443}
444
445/// Static wording for diagnostics and hover about yanked/deprecated package state.
446///
447/// Implementors guarantee every method here returns a `'static` string with no per-call
448/// computation — this is display copy, not logic — so callers may cache or repeat these
449/// values freely across an entire diagnostics pass without re-invoking the formatter.
450pub trait DiagnosticMessages: Send + Sync {
451    /// Message for yanked/deprecated versions in diagnostics.
452    fn yanked_message(&self) -> &'static str {
453        "This version has been yanked"
454    }
455
456    /// Label for yanked versions in hover.
457    fn yanked_label(&self) -> &'static str {
458        "*(yanked)*"
459    }
460
461    /// Message for a package-level deprecation/abandonment diagnostic (issue #205).
462    ///
463    /// Distinct from [`Self::yanked_message`]: that one describes a single flagged
464    /// *version*, this one describes the *package* being deprecated/abandoned/archived.
465    /// Default wording is generic; `ComposerFormatter` overrides both this and
466    /// [`Self::deprecated_label`] to "abandoned", matching Packagist's own vocabulary —
467    /// the same pattern it already applies to the yanked pair.
468    fn deprecated_message(&self) -> &'static str {
469        "This package is deprecated"
470    }
471
472    /// Label for a deprecated package in hover.
473    fn deprecated_label(&self) -> &'static str {
474        "*(deprecated)*"
475    }
476}
477
478/// Per-ecosystem opt-outs for which diagnostics apply to which dependency/requirement shapes.
479///
480/// Implementors guarantee these hooks only ever narrow or disable a diagnostic a shared,
481/// ecosystem-agnostic pass would otherwise emit unconditionally — never widen or fabricate one.
482/// An override does not always mean "this ecosystem is broken": `NpmFormatter` returns `false`
483/// from [`yanked_diagnostic_applies_to`](Self::yanked_diagnostic_applies_to) unconditionally not
484/// because the underlying signal is wrong, but to avoid duplicating the separate #205
485/// package-level deprecation diagnostic that would otherwise fire alongside it.
486pub trait DiagnosticPolicy: Send + Sync {
487    /// Whether this ecosystem's requirement/version syntax follows strict SemVer 2.0.0
488    /// pre-release semantics: a pre-release version (`X.Y.Z-pre`) is excluded from matching
489    /// `requirement` unless `requirement` itself pins to the same `X.Y.Z` tuple with a
490    /// pre-release tag — the rule Cargo's `semver` crate and npm's `node-semver` both
491    /// implement, and that `compile_requirement`'s matcher inherits from its underlying
492    /// comparator.
493    ///
494    /// Used by [`crate::lsp_helpers::requirement_is_unsatisfiable`]'s caller in `generate_diagnostics_from_cache`
495    /// to decide whether the unsatisfiable-requirement WARNING should be enriched with a
496    /// mention of a published pre-release that would satisfy `requirement` if pre-release
497    /// exclusion were relaxed (#299). Maven/NuGet/Composer/Gradle use non-strict,
498    /// ecosystem-specific range models where this premise does not hold — they must not
499    /// override this.
500    ///
501    /// Default `false`. `deps-cargo`, `deps-npm`, and `deps-swift` override this to `true`.
502    ///
503    /// # Examples
504    ///
505    /// ```
506    /// use deps_core::lsp_helpers::DiagnosticPolicy;
507    ///
508    /// struct DefaultFormatter;
509    /// impl DiagnosticPolicy for DefaultFormatter {}
510    ///
511    /// assert!(!DefaultFormatter.strict_semver_prerelease_exclusion());
512    /// ```
513    fn strict_semver_prerelease_exclusion(&self) -> bool {
514        false
515    }
516
517    /// Whether this ecosystem's deprecation payload ([`crate::Deprecation::replacement`])
518    /// is safe to offer as a "Replace with X" rename quickfix.
519    ///
520    /// Default `false`. Only an ecosystem whose replacement name comes from a
521    /// **structured, registry-validated** field may override this to `true` — never one
522    /// synthesized by parsing free text, which is a typosquatting vector (npm's
523    /// `deprecated` message names a successor only in prose). `ComposerFormatter`
524    /// overrides this to `true`: Packagist's `abandoned` replacement is a real package
525    /// name field, not extracted text.
526    fn supports_package_rename(&self) -> bool {
527        false
528    }
529
530    /// Whether the "requirement satisfiable only by a yanked version" diagnostic
531    /// (`crate::lsp_helpers::requirement_matches_only_yanked`) should evaluate `requirement`
532    /// at all for this ecosystem.
533    ///
534    /// Default `true` — no restriction, every requirement shape is checked. Override to
535    /// `false` for a requirement shape (or, returning `false` unconditionally, for every
536    /// requirement) where this diagnostic would duplicate a more specific one, or where this
537    /// ecosystem's `Version::removal_status()` is not a reliable enough per-version signal.
538    /// This is independent of
539    /// [`Registry::reports_yanked`](crate::Registry::reports_yanked): that flag gates whether
540    /// `removal_status()` data is trusted at all (and thus whether the separate #263
541    /// in-use-version yanked check runs), while this hook only narrows *this* diagnostic.
542    ///
543    /// `dep` is passed alongside `requirement` (rather than `requirement` alone) so an
544    /// implementor can key its decision off the dependency's package name — needed by
545    /// `DenoFormatter` (#448) to tell its `jsr:`- and `npm:`-scheme specifiers apart, since
546    /// the scheme lives in the name, not in the requirement text. At the sole call site
547    /// (`crate::lsp_helpers::diagnostics::generate_diagnostics_from_cache`), `requirement`
548    /// is always `dep.version_requirement().unwrap()` for the same `dep` — the two are
549    /// never independent, though an implementor is free to key off either or both.
550    ///
551    /// `DenoFormatter` returns `false` unconditionally for `npm:` specifiers, mirroring
552    /// `NpmFormatter` (#448), and applies unconditionally (`true`, the same as leaving this
553    /// hook at its default) for `jsr:` specifiers, for any requirement shape (#454): unlike
554    /// npm's `deprecated`, JSR's `yanked` flag is a genuine per-version signal with no
555    /// package-level deprecation diagnostic to conflate with, so `jsr:` needs no restriction
556    /// here at all — see that formatter's docs. `NpmFormatter` returns `false`
557    /// unconditionally (#436): npm's `AdvisoryDeprecated` is genuinely per-version but
558    /// commonly applied package-wide, so even an exact pin would often just duplicate the
559    /// dedicated package-level deprecation diagnostic ([`DiagnosticMessages::deprecated_message`],
560    /// issue #205); npm keeps `reports_yanked() == true`; so the #263 in-use-version check
561    /// stays live. `ComposerFormatter` does not override this hook at all — it opts out at
562    /// the registry level instead
563    /// ([`Registry::reports_yanked`](crate::Registry::reports_yanked) `== false`, pre-dating
564    /// #436, independently justified by #233 R2): Packagist's `abandoned` is package-level via
565    /// p2 minified inheritance, so its yanked map is never populated and this hook has nothing
566    /// to restrict.
567    ///
568    /// # Examples
569    ///
570    /// ```
571    /// use deps_core::lsp_helpers::DiagnosticPolicy;
572    /// use deps_core::{Dependency, PackageName, VersionReq};
573    ///
574    /// struct DefaultFormatter;
575    /// impl DiagnosticPolicy for DefaultFormatter {}
576    ///
577    /// # struct FakeDep(PackageName);
578    /// # impl Dependency for FakeDep {
579    /// #     fn name(&self) -> &PackageName {
580    /// #         &self.0
581    /// #     }
582    /// #     fn name_range(&self) -> tower_lsp_server::ls_types::Range {
583    /// #         tower_lsp_server::ls_types::Range::default()
584    /// #     }
585    /// #     fn version_requirement(&self) -> Option<&VersionReq> {
586    /// #         None
587    /// #     }
588    /// #     fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range> {
589    /// #         None
590    /// #     }
591    /// #     fn source(&self) -> deps_core::parser::DependencySource {
592    /// #         deps_core::parser::DependencySource::Registry
593    /// #     }
594    /// #     fn as_any(&self) -> &dyn std::any::Any {
595    /// #         self
596    /// #     }
597    /// # }
598    /// #
599    /// let dep = FakeDep(PackageName::new("example"));
600    /// assert!(DefaultFormatter.yanked_diagnostic_applies_to(&dep, &VersionReq::new("^1.2")));
601    /// ```
602    fn yanked_diagnostic_applies_to(
603        &self,
604        _dep: &dyn Dependency,
605        _requirement: &VersionReq,
606    ) -> bool {
607        true
608    }
609}
610
611/// What a [`DependencySource`](crate::parser::DependencySource) may be used for: resolution,
612/// vulnerability scanning, and cache-key/link trust.
613///
614/// Implementors guarantee [`can_resolve_source`](Self::can_resolve_source) and
615/// [`source_is_public_registry_content`](Self::source_is_public_registry_content) answer
616/// independent questions — a source can be resolvable without being public-registry content
617/// (e.g. a non-mirroring alternate registry), so callers must not assume one implies the
618/// other.
619pub trait SourcePolicy: Send + Sync {
620    /// Whether this ecosystem's registry can resolve version data for `source`.
621    ///
622    /// Hover, diagnostics, and code actions gate every registry lookup on this instead of
623    /// [`crate::parser::DependencySource::is_version_resolvable`] directly, so an ecosystem
624    /// whose `Registry` implementation routes *more* sources than the generic
625    /// crates.io-shaped default (e.g. `deps-cargo`'s `CargoRegistry`, which additionally
626    /// resolves a `DependencySource::AlternateRegistry` against a private sparse index) can
627    /// opt those sources in without widening the `Registry` trait itself or touching any of
628    /// this hook's call sites.
629    ///
630    /// Default: delegates to
631    /// [`DependencySource::is_version_resolvable`](crate::parser::DependencySource::is_version_resolvable),
632    /// so every ecosystem that does not override this method keeps its exact pre-existing
633    /// resolvability answer.
634    ///
635    /// # Examples
636    ///
637    /// ```
638    /// use deps_core::lsp_helpers::SourcePolicy;
639    /// use deps_core::parser::DependencySource;
640    ///
641    /// struct DefaultFormatter;
642    /// impl SourcePolicy for DefaultFormatter {}
643    ///
644    /// assert!(DefaultFormatter.can_resolve_source(&DependencySource::Registry));
645    /// assert!(!DefaultFormatter.can_resolve_source(&DependencySource::AlternateRegistry {
646    ///     index: "https://index.mycorp.dev".into(),
647    ///     mirrors_crates_io: false,
648    /// }));
649    /// ```
650    fn can_resolve_source(&self, source: &crate::parser::DependencySource) -> bool {
651        source.is_version_resolvable()
652    }
653
654    /// Whether `source`'s content is exactly the default public registry's — safe to treat
655    /// as such for OSV vulnerability scanning, cache-key signature construction, and hover
656    /// heading links.
657    ///
658    /// Default `matches!(source, DependencySource::Registry)` — every ecosystem with only
659    /// one registry concept keeps its existing behavior. `deps-cargo`'s `CargoFormatter`
660    /// overrides this to also accept `AlternateRegistry { mirrors_crates_io: true, .. }`:
661    /// Cargo verifies per-version checksum equality against crates.io for a
662    /// `[source.crates-io] replace-with` mirror, so its content is exactly as trustworthy as
663    /// crates.io's own, even though the fetch itself goes to the mirror's index, not to
664    /// crates.io (plan `.local/specs/023-cargo-custom-registries/plan-1b.md` §1.3, F1/F1b/F2).
665    ///
666    /// Deliberately distinct from [`Self::can_resolve_source`]: an `AlternateRegistry` that
667    /// is *not* a crates.io mirror is resolvable (this LSP can fetch its version data) but is
668    /// not public-registry content (its data must not be treated as crates.io's own for
669    /// vulnerability-advisory or link purposes) — the two questions are orthogonal, and a
670    /// single hook conflating them would force every non-Cargo ecosystem to answer a
671    /// mirror-specific question it has no concept of.
672    ///
673    /// # Examples
674    ///
675    /// ```
676    /// use deps_core::lsp_helpers::SourcePolicy;
677    /// use deps_core::parser::DependencySource;
678    ///
679    /// struct DefaultFormatter;
680    /// impl SourcePolicy for DefaultFormatter {}
681    ///
682    /// assert!(DefaultFormatter.source_is_public_registry_content(&DependencySource::Registry));
683    /// assert!(!DefaultFormatter.source_is_public_registry_content(&DependencySource::AlternateRegistry {
684    ///     index: "https://index.mycorp.dev".into(),
685    ///     mirrors_crates_io: true,
686    /// }));
687    /// ```
688    fn source_is_public_registry_content(&self, source: &crate::parser::DependencySource) -> bool {
689        matches!(source, crate::parser::DependencySource::Registry)
690    }
691}
692
693/// Native <-> OSV.dev namespace bridging for package names and version strings.
694///
695/// Implementors guarantee every method is the identity transform unless this ecosystem's
696/// native naming/versioning genuinely diverges from OSV.dev's own convention for it — callers
697/// (the OSV scan-target builder and advisory matcher) rely on the defaults being safe no-ops
698/// for the common case of an ecosystem with no such divergence.
699pub trait OsvNaming: Send + Sync {
700    /// OSV.dev's canonical spelling for `dep`'s package name, or `None` if
701    /// this dependency cannot be mapped (e.g. a non-GitHub Swift package).
702    ///
703    /// Deliberately **not** routed through [`PackageNaming::normalize_package_name`]:
704    /// that method produces this project's internal lookup key, while this
705    /// one produces the name sent on the wire to OSV. They coincide for most
706    /// ecosystems and diverge for NuGet (case-preserving; normalizing would
707    /// lowercase it and zero out results), Composer (OSV wants lowercase,
708    /// overridden in `deps-composer`), and Swift (prefixed to
709    /// `github.com/{owner}/{repo}`, overridden in `deps-swift`). Takes
710    /// `&dyn Dependency` rather than `&str` because the Swift override needs
711    /// to downcast to inspect the dependency's source URL host — see
712    /// `architecture.md` §2.
713    ///
714    /// The default implementation is the identity: OSV is case-sensitive in
715    /// every ecosystem this project supports except PyPI, and for Cargo, npm,
716    /// Go, Maven, Gradle, Dart, Bundler, NuGet, and PyPI the manifest's raw
717    /// name already matches OSV's canonical spelling.
718    fn osv_package_name(&self, dep: &dyn Dependency) -> Option<String> {
719        Some(dep.name().to_string())
720    }
721
722    /// Converts a version string as it appears in an OSV advisory record
723    /// (e.g. [`crate::osv::Advisory::fixed_versions`]) into this ecosystem's
724    /// own version namespace, as used in manifests and by the registry.
725    ///
726    /// Default: identity — correct for ecosystems whose OSV records carry
727    /// the native version string verbatim. Override when OSV's namespace
728    /// diverges from the native one (Go module versions carry a `v` prefix
729    /// that OSV's SEMVER ranges never use).
730    ///
731    /// # Examples
732    ///
733    /// ```
734    /// use deps_core::lsp_helpers::OsvNaming;
735    ///
736    /// struct DefaultFormatter;
737    /// impl OsvNaming for DefaultFormatter {}
738    ///
739    /// assert_eq!(DefaultFormatter.osv_version_to_native("1.2.3"), "1.2.3");
740    /// ```
741    fn osv_version_to_native(&self, version: &str) -> String {
742        version.to_string()
743    }
744
745    /// Rewrites a native-ecosystem version string into the spelling OSV.dev's
746    /// SEMVER range matching expects.
747    ///
748    /// Deliberately the inverse of [`Self::osv_package_name`] rather than a
749    /// field on [`crate::osv::ScanTarget`] itself: the caller (`deps-lsp`'s
750    /// scan-target builder) has only the native version string at hand, so
751    /// each ecosystem's formatter is the natural place to own the transform.
752    /// The default implementation is the identity: OSV accepts every
753    /// supported ecosystem's native version spelling unchanged except Go,
754    /// whose module versions carry a mandatory `v` prefix
755    /// (`golang.org/x/mod/module` convention) that OSV's SEMVER matcher
756    /// rejects — overridden in `deps-go` to strip it.
757    ///
758    /// # Examples
759    ///
760    /// ```
761    /// use deps_core::lsp_helpers::OsvNaming;
762    ///
763    /// struct DefaultFormatter;
764    /// impl OsvNaming for DefaultFormatter {}
765    ///
766    /// assert_eq!(DefaultFormatter.osv_version("1.2.3"), "1.2.3");
767    /// ```
768    fn osv_version(&self, version: &str) -> String {
769        version.to_string()
770    }
771}
772
773/// Umbrella marker for a complete ecosystem formatter.
774///
775/// This trait is intentionally empty: it exists only so `&dyn EcosystemFormatter` keeps
776/// working as a single trait-object type at every existing call site
777/// (`Ecosystem::formatter`, hover, diagnostics, code actions, code lenses, inlay hints,
778/// in-use-version resolution, and OSV scan-target construction). Implementors never write
779/// `impl EcosystemFormatter for X` directly — the blanket impl below supplies it
780/// automatically for any type implementing all seven concern traits
781/// ([`PackageNaming`], [`PackageRendering`], [`RequirementResolution`],
782/// [`DiagnosticMessages`], [`DiagnosticPolicy`], [`SourcePolicy`], [`OsvNaming`]). To add a
783/// new ecosystem formatter, implement those seven traits; to call one specific behavior
784/// (e.g. in a test mock), implement only the trait that owns it.
785pub trait EcosystemFormatter:
786    PackageNaming
787    + PackageRendering
788    + RequirementResolution
789    + DiagnosticMessages
790    + DiagnosticPolicy
791    + SourcePolicy
792    + OsvNaming
793{
794}
795
796impl<
797    T: PackageNaming
798        + PackageRendering
799        + RequirementResolution
800        + DiagnosticMessages
801        + DiagnosticPolicy
802        + SourcePolicy
803        + OsvNaming,
804> EcosystemFormatter for T
805{
806}