use crate::lsp_helpers::{
DiagnosticMessages, DiagnosticPolicy, OsvNaming, PackageNaming, PackageRendering,
RequirementResolution, SourcePolicy,
};
use crate::{ConcreteVersion, Dependency, PackageName};
#[expect(
clippy::struct_excessive_bools,
reason = "each field is an independent, non-exclusive knob, not overlapping state a \
two-variant enum could express more clearly"
)]
#[derive(Debug, Clone, Copy)]
pub struct StubFormatter {
quoted_text_edit: bool,
lowercase_names: bool,
package_url_prefix: &'static str,
manifest_requirement_as_resolved_version: bool,
alternate_registry_resolution: bool,
go_style_osv_version_to_native: bool,
}
impl StubFormatter {
pub const DEFAULT: Self = Self::new();
#[must_use]
pub const fn new() -> Self {
Self {
quoted_text_edit: false,
lowercase_names: false,
package_url_prefix: "https://example.com/",
manifest_requirement_as_resolved_version: false,
alternate_registry_resolution: false,
go_style_osv_version_to_native: false,
}
}
#[must_use]
pub const fn with_quoted_text_edit(mut self) -> Self {
self.quoted_text_edit = true;
self
}
#[must_use]
pub const fn with_lowercase_names(mut self) -> Self {
self.lowercase_names = true;
self
}
#[must_use]
pub const fn with_package_url_prefix(mut self, prefix: &'static str) -> Self {
self.package_url_prefix = prefix;
self
}
#[must_use]
pub const fn with_manifest_requirement_as_resolved_version(mut self) -> Self {
self.manifest_requirement_as_resolved_version = true;
self
}
#[must_use]
pub const fn with_alternate_registry_resolution(mut self) -> Self {
self.alternate_registry_resolution = true;
self
}
#[must_use]
pub const fn with_go_style_osv_version_to_native(mut self) -> Self {
self.go_style_osv_version_to_native = true;
self
}
}
impl Default for StubFormatter {
fn default() -> Self {
Self::new()
}
}
impl PackageNaming for StubFormatter {
fn normalize_package_name(&self, name: &PackageName) -> String {
if self.lowercase_names {
name.as_str().to_lowercase()
} else {
name.as_str().to_string()
}
}
}
impl PackageRendering for StubFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
if self.quoted_text_edit {
format!("\"{version}\"")
} else {
version.to_string()
}
}
fn package_url(&self, name: &PackageName) -> String {
format!("{}{}", self.package_url_prefix, name.as_str())
}
}
impl RequirementResolution for StubFormatter {
fn manifest_requirement_is_resolved_version(&self, dep: &dyn Dependency) -> bool {
let _ = dep;
self.manifest_requirement_as_resolved_version
}
}
impl DiagnosticMessages for StubFormatter {}
impl DiagnosticPolicy for StubFormatter {}
impl SourcePolicy for StubFormatter {
fn resolves_alternate_registry(&self) -> bool {
self.alternate_registry_resolution
}
}
impl OsvNaming for StubFormatter {
fn osv_version_to_native(&self, version: &crate::osv::OsvVersion) -> ConcreteVersion {
let version = version.as_str();
if self.go_style_osv_version_to_native {
if version.starts_with('v') {
ConcreteVersion::new(version)
} else {
ConcreteVersion::new(format!("v{version}"))
}
} else {
ConcreteVersion::new(version)
}
}
}
#[cfg(all(test, feature = "test-util"))]
mod tests {
use super::StubFormatter;
use crate::lsp_helpers::{
PackageNaming, PackageRendering, RequirementResolution, SourcePolicy,
};
use crate::parser::DependencySource;
use crate::{ConcreteVersion, Dependency, PackageName};
struct StubDep;
impl Dependency for StubDep {
fn name(&self) -> &PackageName {
static NAME: std::sync::OnceLock<PackageName> = std::sync::OnceLock::new();
NAME.get_or_init(|| PackageName::new("stub-dep"))
}
fn name_range(&self) -> crate::position::Range {
crate::position::Range::default()
}
fn version_requirement(&self) -> Option<&crate::VersionReq> {
None
}
fn version_range(&self) -> Option<crate::position::Range> {
None
}
fn source(&self) -> DependencySource {
DependencySource::Registry
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
struct DefaultInheriting;
impl PackageNaming for DefaultInheriting {}
impl PackageRendering for DefaultInheriting {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
format!("https://example.com/{}", name.as_str())
}
}
impl RequirementResolution for DefaultInheriting {}
impl crate::lsp_helpers::DiagnosticMessages for DefaultInheriting {}
impl crate::lsp_helpers::DiagnosticPolicy for DefaultInheriting {}
impl SourcePolicy for DefaultInheriting {}
impl crate::lsp_helpers::OsvNaming for DefaultInheriting {}
#[test]
fn default_matches_trait_defaults() {
let stub = StubFormatter::DEFAULT;
let baseline = DefaultInheriting;
let dep = StubDep;
for name in ["Serde", "left-pad", "UPPER"] {
let name = PackageName::new(name);
assert_eq!(
stub.normalize_package_name(&name),
baseline.normalize_package_name(&name)
);
}
assert_eq!(
stub.manifest_requirement_is_resolved_version(&dep),
baseline.manifest_requirement_is_resolved_version(&dep)
);
assert_eq!(
stub.resolves_alternate_registry(),
baseline.resolves_alternate_registry()
);
for source in [
DependencySource::Registry,
DependencySource::AlternateRegistry {
index: "https://index.mycorp.dev".into(),
mirrors_crates_io: false,
},
] {
assert_eq!(
stub.can_resolve_source(&source),
baseline.can_resolve_source(&source),
"diverged for {source:?}"
);
}
}
#[test]
fn default_is_unquoted_identity_example_com() {
let stub = StubFormatter::DEFAULT;
let name = PackageName::new("serde");
assert_eq!(
stub.format_version_for_text_edit(&ConcreteVersion::new("1.2.3")),
"1.2.3"
);
assert_eq!(stub.normalize_package_name(&name), "serde");
assert_eq!(stub.package_url(&name), "https://example.com/serde");
assert!(!stub.manifest_requirement_is_resolved_version(&StubDep));
assert!(!stub.resolves_alternate_registry());
}
#[test]
fn with_quoted_text_edit_wraps_in_quotes() {
const F: StubFormatter = StubFormatter::new().with_quoted_text_edit();
assert_eq!(
F.format_version_for_text_edit(&ConcreteVersion::new("1.2.3")),
"\"1.2.3\""
);
}
#[test]
fn with_lowercase_names_normalizes_to_lowercase() {
const F: StubFormatter = StubFormatter::new().with_lowercase_names();
assert_eq!(
F.normalize_package_name(&PackageName::new("Newtonsoft.Json")),
"newtonsoft.json"
);
}
#[test]
fn with_package_url_prefix_overrides_default_prefix() {
const F: StubFormatter =
StubFormatter::new().with_package_url_prefix("https://pkg.go.dev/");
assert_eq!(
F.package_url(&PackageName::new("golang.org/x/text")),
"https://pkg.go.dev/golang.org/x/text"
);
}
#[test]
fn with_manifest_requirement_as_resolved_version_reports_true() {
const F: StubFormatter =
StubFormatter::new().with_manifest_requirement_as_resolved_version();
assert!(F.manifest_requirement_is_resolved_version(&StubDep));
}
#[test]
fn with_alternate_registry_resolution_widens_can_resolve_source() {
const F: StubFormatter = StubFormatter::new().with_alternate_registry_resolution();
assert!(F.resolves_alternate_registry());
assert!(F.can_resolve_source(&DependencySource::AlternateRegistry {
index: "https://index.mycorp.dev".into(),
mirrors_crates_io: false,
}));
}
}