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}