1use crate::specifier::parse_specifier;
10use crate::types::{DenoDependency, DenoDependencySection};
11use deps_core::lsp_helpers::LineOffsetTable;
12use deps_core::{DepsError, PackageName, Result, VersionReq};
13use jsonc_parser::ast::{Object, ObjectProp, StringLit, Value};
14use jsonc_parser::{CollectOptions, ParseOptions, parse_to_ast};
15use std::borrow::Cow;
16use std::collections::HashSet;
17use tower_lsp_server::ls_types::{Range, Uri};
18
19#[derive(Debug)]
21pub struct DenoParseResult {
22 pub dependencies: Vec<DenoDependency>,
24 pub uri: Uri,
26}
27
28deps_core::impl_parse_result!(
29 DenoParseResult,
30 DenoDependency {
31 dependencies: dependencies,
32 uri: uri,
33 }
34);
35
36pub fn parse_deno_json(content: &str, uri: &Uri) -> Result<DenoParseResult> {
63 let ast = parse_to_ast(
64 content,
65 &CollectOptions::default(),
66 &ParseOptions::default(),
67 )
68 .map_err(|e| DepsError::ParseError {
69 file_type: "deno.json".into(),
70 source: e.to_string().into(),
71 })?;
72
73 let line_table = LineOffsetTable::new(content);
74 let mut dependencies = Vec::new();
75
76 if let Some(Value::Object(root)) = ast.value
77 && let Some(imports_prop) = find_last_prop(&root, "imports")
78 && let Value::Object(imports) = &imports_prop.value
79 {
80 collect_imports(imports, content, &line_table, &mut dependencies);
81 }
82
83 Ok(DenoParseResult {
84 dependencies,
85 uri: uri.clone(),
86 })
87}
88
89fn find_last_prop<'a, 'b>(object: &'a Object<'b>, key: &str) -> Option<&'a ObjectProp<'b>> {
94 object
95 .properties
96 .iter()
97 .rev()
98 .find(|prop| prop.name.as_str() == key)
99}
100
101fn collect_imports(
106 imports: &Object,
107 content: &str,
108 line_table: &LineOffsetTable,
109 out: &mut Vec<DenoDependency>,
110) {
111 let mut seen_aliases = HashSet::new();
112 let mut collected = Vec::new();
113
114 for prop in imports.properties.iter().rev() {
117 let alias = prop.name.as_str();
118 if !seen_aliases.insert(alias.to_string()) {
119 continue;
120 }
121 let Value::StringLit(value_lit) = &prop.value else {
122 continue;
123 };
124 if let Some(dep) = build_dependency(value_lit, content, line_table) {
125 collected.push(dep);
126 }
127 }
128
129 collected.reverse();
131 out.extend(collected);
132}
133
134fn build_dependency(
139 value_lit: &StringLit,
140 content: &str,
141 line_table: &LineOffsetTable,
142) -> Option<DenoDependency> {
143 let raw_value = value_lit.value.as_ref();
144
145 if let Cow::Owned(_) = &value_lit.value {
154 let parsed = parse_specifier(raw_value)?;
155 tracing::debug!(
156 "deno.json: import value contains escape sequences, skipping precise position tracking"
157 );
158 let inner_start = value_lit.range.start + 1;
159 let inner_end = value_lit.range.end.saturating_sub(1);
160 return Some(DenoDependency {
161 name: PackageName::new(parsed.name),
162 name_range: byte_range_to_lsp(content, line_table, inner_start, inner_end),
163 version_req: parsed.version_req.map(VersionReq::new),
164 version_range: None,
165 section: DenoDependencySection::Imports,
166 });
167 }
168
169 let value_start = value_lit.range.start + 1;
171
172 if let Some(parsed) = parse_specifier(raw_value) {
173 let name_range = byte_range_to_lsp(
174 content,
175 line_table,
176 value_start + parsed.name_range.start,
177 value_start + parsed.name_range.end,
178 );
179 let version_range = parsed.version_range.map(|r| {
180 byte_range_to_lsp(
181 content,
182 line_table,
183 value_start + r.start,
184 value_start + r.end,
185 )
186 });
187
188 return Some(DenoDependency {
189 name: PackageName::new(parsed.name),
190 name_range,
191 version_req: parsed.version_req.map(VersionReq::new),
192 version_range,
193 section: DenoDependencySection::Imports,
194 });
195 }
196
197 let partial_range = crate::specifier::partial_name_range(raw_value)?;
206 let name_range = byte_range_to_lsp(
207 content,
208 line_table,
209 value_start + partial_range.start,
210 value_start + partial_range.end,
211 );
212
213 Some(DenoDependency {
214 name: PackageName::new(&raw_value[partial_range]),
215 name_range,
216 version_req: None,
217 version_range: None,
218 section: DenoDependencySection::Imports,
219 })
220}
221
222fn byte_range_to_lsp(content: &str, table: &LineOffsetTable, start: usize, end: usize) -> Range {
224 Range::new(
225 table.byte_offset_to_position(content, start),
226 table.byte_offset_to_position(content, end),
227 )
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 use std::assert_matches;
235
236 fn test_uri() -> Uri {
237 deps_core::test_util::test_uri("/test/deno.json")
238 }
239
240 #[test]
241 fn test_parse_simple_jsr_import() {
242 let json = r#"{
243 "imports": {
244 "@std/fs": "jsr:@std/fs@^1.0"
245 }
246}"#;
247
248 let result = parse_deno_json(json, &test_uri()).unwrap();
249 assert_eq!(result.dependencies.len(), 1);
250
251 let dep = &result.dependencies[0];
252 assert_eq!(dep.name, "jsr:@std/fs");
253 assert_eq!(dep.version_req, Some("^1.0".into()));
254 assert_matches!(dep.section, DenoDependencySection::Imports);
255 }
256
257 #[test]
258 fn test_parse_npm_import() {
259 let json = r#"{
260 "imports": {
261 "react": "npm:react@^18"
262 }
263}"#;
264
265 let result = parse_deno_json(json, &test_uri()).unwrap();
266 assert_eq!(result.dependencies.len(), 1);
267 assert_eq!(result.dependencies[0].name, "npm:react");
268 assert_eq!(result.dependencies[0].version_req, Some("^18".into()));
269 }
270
271 #[test]
272 fn test_parse_mixed_jsr_and_npm() {
273 let json = r#"{
274 "imports": {
275 "@std/fs": "jsr:@std/fs@^1.0",
276 "react": "npm:react@^18",
277 "preact": "npm:preact@^10"
278 }
279}"#;
280
281 let result = parse_deno_json(json, &test_uri()).unwrap();
282 assert_eq!(result.dependencies.len(), 3);
283 assert!(result.dependencies.iter().any(|d| d.name == "jsr:@std/fs"));
284 assert!(result.dependencies.iter().any(|d| d.name == "npm:react"));
285 assert!(result.dependencies.iter().any(|d| d.name == "npm:preact"));
286 }
287
288 #[test]
289 fn test_parse_empty_imports() {
290 let json = r#"{"imports": {}}"#;
291 let result = parse_deno_json(json, &test_uri()).unwrap();
292 assert!(result.dependencies.is_empty());
293 }
294
295 #[test]
296 fn test_parse_no_imports_key() {
297 let json = r#"{"name": "my-app"}"#;
298 let result = parse_deno_json(json, &test_uri()).unwrap();
299 assert!(result.dependencies.is_empty());
300 }
301
302 #[test]
303 fn test_parse_unsupported_specifier_silently_skipped() {
304 let json = r#"{
305 "imports": {
306 "@std/fs": "jsr:@std/fs@^1.0",
307 "legacy": "https://deno.land/x/legacy/mod.ts",
308 "local": "./local.ts"
309 }
310}"#;
311
312 let result = parse_deno_json(json, &test_uri()).unwrap();
313 assert_eq!(result.dependencies.len(), 1);
314 assert_eq!(result.dependencies[0].name, "jsr:@std/fs");
315 }
316
317 #[test]
318 fn test_parse_invalid_json_errors() {
319 let json = "{ imports: not valid json !!!";
320 let result = parse_deno_json(json, &test_uri());
321 assert_matches!(
322 result,
323 Err(DepsError::ParseError { file_type, .. }) if file_type == "deno.json"
324 );
325 }
326
327 #[test]
328 fn test_parse_jsonc_comments_tolerated() {
329 let jsonc = r#"{
330 // line comment
331 "imports": {
332 /* block comment */
333 "@std/fs": "jsr:@std/fs@^1.0"
334 }
335}"#;
336
337 let result = parse_deno_json(jsonc, &test_uri()).unwrap();
338 assert_eq!(result.dependencies.len(), 1);
339 assert_eq!(result.dependencies[0].name, "jsr:@std/fs");
340 }
341
342 #[test]
343 fn test_duplicate_alias_keys_last_wins() {
344 let json = r#"{
346 "imports": {
347 "@std/fs": "jsr:@std/fs@^1.0",
348 "@std/fs": "jsr:@std/fs@^2.0"
349 }
350}"#;
351
352 let result = parse_deno_json(json, &test_uri()).unwrap();
353 assert_eq!(result.dependencies.len(), 1);
354 assert_eq!(result.dependencies[0].version_req, Some("^2.0".into()));
355 assert_eq!(result.dependencies[0].name_range.start.line, 3);
357 }
358
359 #[test]
360 fn test_two_aliases_same_specifier_get_distinct_positions() {
361 let json = r#"{
365 "imports": {
366 "@std/fs": "jsr:@std/fs@^1.0",
367 "@std/fs/": "jsr:@std/fs@^1.0"
368 }
369}"#;
370
371 let result = parse_deno_json(json, &test_uri()).unwrap();
372 assert_eq!(result.dependencies.len(), 2);
373 let lines: Vec<u32> = result
374 .dependencies
375 .iter()
376 .map(|d| d.name_range.start.line)
377 .collect();
378 assert_ne!(lines[0], lines[1]);
379 }
380
381 #[test]
382 fn test_empty_version_after_at_produces_a_version_range() {
383 let json = r#"{"imports": {"@std/fs": "jsr:@std/fs@"}}"#;
386 let result = parse_deno_json(json, &test_uri()).unwrap();
387 assert_eq!(result.dependencies.len(), 1);
388 let dep = &result.dependencies[0];
389 assert_eq!(dep.version_req, Some(String::new().into()));
390 let range = dep.version_range.expect("version_range must be Some");
391 assert_eq!(range.start, range.end);
392 }
393
394 #[test]
395 fn test_scoped_npm_import() {
396 let json = r#"{"imports": {"node-types": "npm:@types/node@^20"}}"#;
397 let result = parse_deno_json(json, &test_uri()).unwrap();
398 assert_eq!(result.dependencies.len(), 1);
399 assert_eq!(result.dependencies[0].name, "npm:@types/node");
400 assert_eq!(result.dependencies[0].version_req, Some("^20".into()));
401 }
402
403 #[test]
404 fn test_escaped_specifier_falls_back_to_inner_span_with_no_version_range() {
405 let json_escape = "\\u0031"; let raw_value = format!("jsr:@std/fs@{json_escape}.0"); let json = format!(r#"{{"imports": {{"@std/fs": "{raw_value}"}}}}"#);
414 let result = parse_deno_json(&json, &test_uri()).unwrap();
415 assert_eq!(result.dependencies.len(), 1);
416
417 let dep = &result.dependencies[0];
418 assert_eq!(dep.name, "jsr:@std/fs");
419 assert_eq!(dep.version_req, Some("1.0".into()));
420 assert!(dep.version_range.is_none());
421
422 let value_start_byte = json.find(&format!("\"{raw_value}\"")).unwrap() + 1;
426 let value_end_byte = value_start_byte + raw_value.len();
427
428 assert_eq!(dep.name_range.start.line, 0);
429 assert_eq!(dep.name_range.start.character, value_start_byte as u32);
430 assert_eq!(dep.name_range.end.character, value_end_byte as u32);
431 }
432
433 #[test]
436 fn test_partial_jsr_bare_scheme_produces_dependency_with_no_version() {
437 let json = r#"{"imports": {"@std/fs": "jsr:"}}"#;
438 let result = parse_deno_json(json, &test_uri()).unwrap();
439 assert_eq!(result.dependencies.len(), 1);
440 let dep = &result.dependencies[0];
441 assert_eq!(dep.name, "jsr:");
442 assert_eq!(dep.version_req, None);
443 assert_eq!(dep.version_range, None);
444 }
445
446 #[test]
447 fn test_partial_jsr_scope_started_produces_dependency() {
448 let json = r#"{"imports": {"@std/fs": "jsr:@"}}"#;
449 let result = parse_deno_json(json, &test_uri()).unwrap();
450 assert_eq!(result.dependencies.len(), 1);
451 assert_eq!(result.dependencies[0].name, "jsr:@");
452 }
453
454 #[test]
455 fn test_partial_jsr_scope_in_progress_produces_dependency() {
456 let json = r#"{"imports": {"@std/fs": "jsr:@std"}}"#;
457 let result = parse_deno_json(json, &test_uri()).unwrap();
458 assert_eq!(result.dependencies.len(), 1);
459 assert_eq!(result.dependencies[0].name, "jsr:@std");
460 }
461
462 #[test]
463 fn test_partial_jsr_trailing_slash_produces_dependency() {
464 let json = r#"{"imports": {"@std/fs": "jsr:@std/"}}"#;
465 let result = parse_deno_json(json, &test_uri()).unwrap();
466 assert_eq!(result.dependencies.len(), 1);
467 assert_eq!(result.dependencies[0].name, "jsr:@std/");
468 }
469
470 #[test]
471 fn test_partial_specifier_name_range_covers_the_typed_text() {
472 let json = r#"{"imports": {"@std/fs": "jsr:@std/"}}"#;
475 let result = parse_deno_json(json, &test_uri()).unwrap();
476 let dep = &result.dependencies[0];
477 let value_start = json.find(r#""jsr:@std/""#).unwrap() + 1;
478 assert_eq!(dep.name_range.start.character, value_start as u32);
479 assert_eq!(
480 dep.name_range.end.character,
481 (value_start + "jsr:@std/".len()) as u32
482 );
483 }
484
485 #[test]
486 fn test_permanently_malformed_empty_scope_still_skipped() {
487 let json = r#"{"imports": {"@std/fs": "jsr:@/pkg"}}"#;
490 let result = parse_deno_json(json, &test_uri()).unwrap();
491 assert!(result.dependencies.is_empty());
492 }
493
494 #[test]
495 fn test_unsupported_scheme_still_skipped_not_treated_as_partial() {
496 let json = r#"{"imports": {"legacy": "https://deno.land/x/legacy/mod.ts"}}"#;
497 let result = parse_deno_json(json, &test_uri()).unwrap();
498 assert!(result.dependencies.is_empty());
499 }
500
501 #[test]
502 fn test_partial_specifier_end_to_end_completion_context_fires() {
503 use deps_core::completion::{CompletionContext, detect_completion_context};
504
505 for value in ["jsr:", "jsr:@", "jsr:@std", "jsr:@std/"] {
506 let content = format!(r#"{{"imports": {{"@std/fs": "{value}"}}}}"#);
507 let result = parse_deno_json(&content, &test_uri()).unwrap();
508 assert_eq!(result.dependencies.len(), 1, "value: {value}");
509
510 let name_range = result.dependencies[0].name_range;
511 let cursor = name_range.end; let context = detect_completion_context(&result, cursor, &content);
513
514 match context {
515 CompletionContext::PackageName { prefix, .. } => {
516 assert_eq!(prefix, value, "value: {value}");
517 }
518 other => panic!("value {value}: expected PackageName context, got {other:?}"),
519 }
520 }
521 }
522
523 #[test]
524 fn test_unclosed_block_comment_is_a_parse_error() {
525 let jsonc = r#"{
528 /* unclosed comment
529 "imports": {
530 "@std/fs": "jsr:@std/fs@^1.0"
531 }
532}"#;
533 let result = parse_deno_json(jsonc, &test_uri());
534 assert!(result.is_err());
535 }
536}