1use crate::specifier::{Scheme, is_dot_prefixed, split_scheme, split_scoped};
11use deps_core::lsp_helpers::{
12 DiagnosticMessages, DiagnosticPolicy, OsvNaming, PackageNaming, PackageRendering,
13 RequirementMatcher, RequirementResolution, SourcePolicy, warn_rejected_value,
14};
15use deps_core::{ConcreteVersion, Dependency, InvalidPackageName, PackageName, VersionReq};
16
17struct NodeSemverMatcher(node_semver::Range);
22
23impl RequirementMatcher for NodeSemverMatcher {
24 fn matches(&self, version: &ConcreteVersion) -> Option<bool> {
25 let version = version.as_str();
26 node_semver::Version::parse(version)
27 .ok()
28 .map(|v| self.0.satisfies(&v))
29 }
30}
31
32const MAX_JSR_SEGMENT_LENGTH: usize = 64;
37
38fn validate_jsr_segment(segment: &str) -> Result<(), InvalidPackageName> {
44 if is_dot_prefixed(segment) {
45 return Err(InvalidPackageName::new(
46 "jsr: scope and package name must not start with '.'",
47 ));
48 }
49 if segment.chars().count() > MAX_JSR_SEGMENT_LENGTH {
50 return Err(InvalidPackageName::new(format!(
51 "jsr: scope/package name cannot exceed {MAX_JSR_SEGMENT_LENGTH} characters"
52 )));
53 }
54 Ok(())
55}
56
57pub struct DenoFormatter;
59
60impl PackageNaming for DenoFormatter {
61 fn validate_package_name(&self, name: &str) -> Result<(), InvalidPackageName> {
66 match split_scheme(name) {
67 Some((Scheme::Jsr, rest)) => {
68 let Some((scope, pkg)) = split_scoped(rest) else {
69 return Err(InvalidPackageName::new(
70 "jsr: packages must be scoped as @scope/name",
71 ));
72 };
73 validate_jsr_segment(scope)?;
74 validate_jsr_segment(pkg)?;
75 Ok(())
76 }
77 Some((Scheme::Npm, rest)) => deps_npm::NpmFormatter.validate_package_name(rest),
78 None => Err(InvalidPackageName::new(
79 "unsupported specifier scheme (expected jsr: or npm:)",
80 )),
81 }
82 }
83}
84
85impl PackageRendering for DenoFormatter {
86 fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
87 let version = version.as_str();
88 version.to_string()
91 }
92
93 fn package_url(&self, name: &PackageName) -> String {
94 match split_scheme(name.as_str()) {
95 Some((Scheme::Jsr, rest)) => match split_scoped(rest) {
96 Some((scope, pkg)) => crate::registry::jsr_package_url(scope, pkg),
97 None => {
98 warn_rejected_value(
99 "split_scoped",
100 "deno jsr package display formatting",
101 name.as_str(),
102 );
103 String::new()
104 }
105 },
106 Some((Scheme::Npm, rest)) => deps_npm::package_url(rest),
107 None => {
108 warn_rejected_value(
109 "split_scheme",
110 "deno package display formatting",
111 name.as_str(),
112 );
113 String::new()
114 }
115 }
116 }
117}
118
119impl RequirementResolution for DenoFormatter {
120 fn compile_requirement(&self, requirement: &VersionReq) -> Option<Box<dyn RequirementMatcher>> {
123 node_semver::Range::parse(requirement.as_str())
124 .ok()
125 .map(|req| Box::new(NodeSemverMatcher(req)) as Box<dyn RequirementMatcher>)
126 }
127}
128
129impl DiagnosticMessages for DenoFormatter {
130 fn yanked_message(&self) -> &'static str {
137 "This version is deprecated"
138 }
139
140 fn yanked_label(&self) -> &'static str {
141 "*(deprecated)*"
142 }
143}
144
145impl DiagnosticPolicy for DenoFormatter {
146 fn yanked_diagnostic_applies_to(
164 &self,
165 dep: &dyn Dependency,
166 _requirement: &VersionReq,
167 ) -> bool {
168 match split_scheme(dep.name().as_str()) {
169 Some((Scheme::Npm, _)) => false,
170 Some((Scheme::Jsr, _)) | None => true,
175 }
176 }
177}
178
179impl SourcePolicy for DenoFormatter {}
180
181impl OsvNaming for DenoFormatter {
182 fn osv_package_name(&self, dep: &dyn Dependency) -> Option<String> {
188 match split_scheme(dep.name().as_str()) {
189 Some((Scheme::Npm, rest)) => Some(rest.to_string()),
190 _ => None,
191 }
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 use deps_core::test_util::capture_tracing_output;
200
201 #[test]
202 fn test_package_url_jsr() {
203 let formatter = DenoFormatter;
204 assert_eq!(
205 formatter.package_url(&PackageName::new("jsr:@std/fs")),
206 "https://jsr.io/@std/fs"
207 );
208 }
209
210 #[test]
211 fn test_package_url_npm() {
212 let formatter = DenoFormatter;
213 assert_eq!(
214 formatter.package_url(&PackageName::new("npm:react")),
215 "https://www.npmjs.com/package/react"
216 );
217 }
218
219 #[test]
220 fn test_package_url_npm_scoped() {
221 let formatter = DenoFormatter;
222 assert_eq!(
223 formatter.package_url(&PackageName::new("npm:@types/node")),
224 "https://www.npmjs.com/package/@types/node"
225 );
226 }
227
228 #[test]
229 fn test_package_url_unroutable_scheme_is_empty() {
230 let formatter = DenoFormatter;
231 assert_eq!(formatter.package_url(&PackageName::new("unknown:x")), "");
232 }
233
234 #[test]
235 fn test_package_url_jsr_unscoped_is_empty() {
236 let formatter = DenoFormatter;
238 assert_eq!(formatter.package_url(&PackageName::new("jsr:std")), "");
239 }
240
241 #[test]
242 fn test_package_url_jsr_unscoped_logs_warn_rejected_value() {
243 let formatter = DenoFormatter;
246 let output = capture_tracing_output(|| {
247 let _ = formatter.package_url(&PackageName::new("jsr:std"));
248 });
249 assert!(output.contains("split_scoped"), "output was: {output}");
250 assert!(
251 output.contains("deno jsr package display formatting"),
252 "output was: {output}"
253 );
254 }
255
256 #[test]
257 fn test_package_url_unroutable_scheme_logs_warn_rejected_value() {
258 let formatter = DenoFormatter;
259 let output = capture_tracing_output(|| {
260 let _ = formatter.package_url(&PackageName::new("unknown:x"));
261 });
262 assert!(output.contains("split_scheme"), "output was: {output}");
263 assert!(
264 output.contains("deno package display formatting"),
265 "output was: {output}"
266 );
267 }
268
269 #[test]
270 fn test_package_url_jsr_accepted_logs_no_warn() {
271 let formatter = DenoFormatter;
272 let output = capture_tracing_output(|| {
273 let _ = formatter.package_url(&PackageName::new("jsr:@std/fs"));
274 });
275 assert!(output.is_empty(), "output was: {output}");
276 }
277
278 #[test]
279 fn test_yanked_wording_matches_npm() {
280 let formatter = DenoFormatter;
281 assert_eq!(formatter.yanked_message(), "This version is deprecated");
282 assert_eq!(formatter.yanked_label(), "*(deprecated)*");
283 }
284
285 #[test]
286 fn test_validate_package_name_jsr_requires_scope() {
287 let formatter = DenoFormatter;
288 assert!(formatter.validate_package_name("jsr:@std/fs").is_ok());
289 assert!(formatter.validate_package_name("jsr:std").is_err());
290 assert!(formatter.validate_package_name("jsr:@std").is_err());
291 }
292
293 #[test]
294 fn test_validate_package_name_jsr_rejects_dot_segments() {
295 let formatter = DenoFormatter;
298 assert!(formatter.validate_package_name("jsr:@a/..").is_err());
299 assert!(formatter.validate_package_name("jsr:@../x").is_err());
300 assert!(formatter.validate_package_name("jsr:@./x").is_err());
301 assert!(formatter.validate_package_name("jsr:@a/.hidden").is_err());
302 }
303
304 #[test]
305 fn test_validate_package_name_jsr_rejects_overlong_segment() {
306 let formatter = DenoFormatter;
307 let too_long = "a".repeat(65);
308 assert!(
309 formatter
310 .validate_package_name(&format!("jsr:@{too_long}/pkg"))
311 .is_err()
312 );
313 assert!(
314 formatter
315 .validate_package_name(&format!("jsr:@scope/{too_long}"))
316 .is_err()
317 );
318 let max_len = "a".repeat(64);
319 assert!(
320 formatter
321 .validate_package_name(&format!("jsr:@{max_len}/pkg"))
322 .is_ok()
323 );
324 }
325
326 #[test]
327 fn test_validate_package_name_npm_delegates_to_npm_rules() {
328 let formatter = DenoFormatter;
329 assert!(formatter.validate_package_name("npm:react").is_ok());
330 assert!(formatter.validate_package_name("npm:@types/node").is_ok());
331 assert!(formatter.validate_package_name("npm:node_modules").is_err());
333 }
334
335 #[test]
336 fn test_validate_package_name_rejects_unknown_scheme() {
337 let formatter = DenoFormatter;
338 assert!(
339 formatter
340 .validate_package_name("https://example.com")
341 .is_err()
342 );
343 }
344
345 #[test]
346 fn test_compile_requirement_satisfiable() {
347 let formatter = DenoFormatter;
348 let matcher = formatter
349 .compile_requirement(&VersionReq::new("^1.0.0"))
350 .expect("valid node-semver range must compile");
351 assert_eq!(matcher.matches(&ConcreteVersion::new("1.5.0")), Some(true));
352 assert_eq!(matcher.matches(&ConcreteVersion::new("2.0.0")), Some(false));
353 }
354
355 #[test]
356 fn test_compile_requirement_dist_tag_returns_none() {
357 let formatter = DenoFormatter;
361 assert!(
362 formatter
363 .compile_requirement(&VersionReq::new("latest"))
364 .is_none()
365 );
366 assert!(
367 formatter
368 .compile_requirement(&VersionReq::new("next"))
369 .is_none()
370 );
371 }
372
373 fn test_dep(name: &str, requirement: &str) -> crate::types::DenoDependency {
377 crate::types::DenoDependency {
378 name: PackageName::new(name),
379 name_range: tower_lsp_server::ls_types::Range::default(),
380 version_req: Some(VersionReq::new(requirement)),
381 version_range: None,
382 section: crate::types::DenoDependencySection::Imports,
383 }
384 }
385
386 #[test]
390 fn test_yanked_diagnostic_applies_to_jsr_scheme_always_true() {
391 let formatter = DenoFormatter;
392 for requirement in ["1.2.3", "^1.2.3", "~1.2.3", "*"] {
393 let dep = test_dep("jsr:@std/fs", requirement);
394 assert!(
395 formatter.yanked_diagnostic_applies_to(&dep, &VersionReq::new(requirement)),
396 "expected {requirement:?} to be accepted for jsr: scheme"
397 );
398 }
399 }
400
401 #[test]
405 fn test_yanked_diagnostic_applies_to_npm_scheme_always_false() {
406 let formatter = DenoFormatter;
407 for requirement in ["1.2.3", "^1.2.3", "~1.2.3", "*"] {
408 let dep = test_dep("npm:lodash", requirement);
409 assert!(
410 !formatter.yanked_diagnostic_applies_to(&dep, &VersionReq::new(requirement)),
411 "expected {requirement:?} to be rejected for npm: scheme"
412 );
413 }
414 }
415
416 #[test]
417 fn test_osv_package_name_npm_maps_to_bare_name() {
418 let formatter = DenoFormatter;
419
420 struct FakeDep(deps_core::PackageName);
421 impl Dependency for FakeDep {
422 fn name(&self) -> &deps_core::PackageName {
423 &self.0
424 }
425 fn name_range(&self) -> tower_lsp_server::ls_types::Range {
426 tower_lsp_server::ls_types::Range::default()
427 }
428 fn version_requirement(&self) -> Option<&VersionReq> {
429 None
430 }
431 fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range> {
432 None
433 }
434 fn source(&self) -> deps_core::parser::DependencySource {
435 deps_core::parser::DependencySource::Registry
436 }
437 fn as_any(&self) -> &dyn std::any::Any {
438 self
439 }
440 }
441
442 let npm_dep = FakeDep(PackageName::new("npm:react"));
443 assert_eq!(
444 formatter.osv_package_name(&npm_dep),
445 Some("react".to_string())
446 );
447
448 let jsr_dep = FakeDep(PackageName::new("jsr:@std/fs"));
449 assert_eq!(formatter.osv_package_name(&jsr_dep), None);
450 }
451}