1use crate::error::{CargoError, Result};
32use crate::types::{DependencySection, DependencySource, ParsedDependency};
33use std::any::Any;
34use std::path::PathBuf;
35use toml_edit::{Document, DocumentMut, Item, Table, Value};
36use tower_lsp_server::ls_types::{Position, Range, Uri};
37
38#[derive(Debug, Clone)]
43pub struct ParseResult {
44 pub dependencies: Vec<ParsedDependency>,
46 pub workspace_root: Option<PathBuf>,
48 pub uri: Uri,
50}
51
52struct LineOffsetTable {
54 line_starts: Vec<usize>,
55}
56
57impl LineOffsetTable {
58 fn new(content: &str) -> Self {
59 let mut line_starts = vec![0];
60 for (i, c) in content.char_indices() {
61 if c == '\n' {
62 line_starts.push(i + 1);
63 }
64 }
65 Self { line_starts }
66 }
67
68 fn byte_offset_to_position(&self, content: &str, offset: usize) -> Position {
69 let line = self
70 .line_starts
71 .partition_point(|&start| start <= offset)
72 .saturating_sub(1);
73 let line_start = self.line_starts[line];
74
75 let character = content[line_start..offset]
76 .chars()
77 .map(|c| c.len_utf16() as u32)
78 .sum();
79
80 Position::new(line as u32, character)
81 }
82}
83
84pub fn parse_cargo_toml(content: &str, doc_uri: &Uri) -> Result<ParseResult> {
109 let doc: Document<&str> =
111 Document::parse(content).map_err(|e| CargoError::TomlParseError { source: e })?;
112
113 let line_table = LineOffsetTable::new(content);
114 let mut dependencies = Vec::new();
115
116 if let Some(deps_item) = doc.get("dependencies")
117 && let Some(deps) = deps_item.as_table()
118 {
119 dependencies.extend(parse_dependencies_section(
120 deps,
121 content,
122 &line_table,
123 DependencySection::Dependencies,
124 )?);
125 }
126
127 if let Some(dev_deps_item) = doc.get("dev-dependencies")
128 && let Some(dev_deps) = dev_deps_item.as_table()
129 {
130 dependencies.extend(parse_dependencies_section(
131 dev_deps,
132 content,
133 &line_table,
134 DependencySection::DevDependencies,
135 )?);
136 }
137
138 if let Some(build_deps_item) = doc.get("build-dependencies")
139 && let Some(build_deps) = build_deps_item.as_table()
140 {
141 dependencies.extend(parse_dependencies_section(
142 build_deps,
143 content,
144 &line_table,
145 DependencySection::BuildDependencies,
146 )?);
147 }
148
149 if let Some(workspace_item) = doc.get("workspace")
151 && let Some(workspace_table) = workspace_item.as_table()
152 && let Some(workspace_deps_item) = workspace_table.get("dependencies")
153 && let Some(workspace_deps) = workspace_deps_item.as_table()
154 {
155 dependencies.extend(parse_dependencies_section(
156 workspace_deps,
157 content,
158 &line_table,
159 DependencySection::WorkspaceDependencies,
160 )?);
161 }
162
163 let workspace_root = find_workspace_root(doc_uri)?;
164
165 Ok(ParseResult {
166 dependencies,
167 workspace_root,
168 uri: doc_uri.clone(),
169 })
170}
171
172fn parse_dependencies_section(
174 table: &Table,
175 content: &str,
176 line_table: &LineOffsetTable,
177 section: DependencySection,
178) -> Result<Vec<ParsedDependency>> {
179 let mut deps = Vec::new();
180
181 for (key, value) in table {
182 let name = key.to_string();
183
184 let name_range = compute_name_range_from_value(content, line_table, &name, value);
185
186 let mut dep = ParsedDependency {
187 name,
188 name_range,
189 version_req: None,
190 version_range: None,
191 features: Vec::new(),
192 features_range: None,
193 source: DependencySource::Registry,
194 workspace_inherited: false,
195 section,
196 };
197
198 match value {
199 Item::Value(Value::String(s)) => {
200 dep.version_req = Some(s.value().clone());
201 if let Some(span) = s.span() {
202 dep.version_range = Some(span_to_range_with_table(
203 content, line_table, span.start, span.end,
204 ));
205 }
206 }
207 Item::Value(Value::InlineTable(t)) => {
208 parse_inline_table_dependency(&mut dep, t, content, line_table)?;
209 }
210 Item::Table(t) => {
211 parse_table_dependency(&mut dep, t, content, line_table)?;
212 }
213 _ => continue,
214 }
215
216 deps.push(dep);
217 }
218
219 Ok(deps)
220}
221
222fn compute_name_range_from_value(
224 content: &str,
225 line_table: &LineOffsetTable,
226 name: &str,
227 value: &Item,
228) -> Range {
229 let value_span = match value {
230 Item::Value(v) => v.span(),
231 Item::Table(t) => t.span(),
232 _ => None,
233 };
234
235 if let Some(span) = value_span {
236 let search_start = span.start.saturating_sub(name.len() + 100);
237 let search_end = span.start;
238
239 if search_start < content.len() && search_end <= content.len() {
240 let search_slice = &content[search_start..search_end];
241
242 if let Some(pos) = search_slice.rfind(name) {
243 let name_start = search_start + pos;
244 let name_end = name_start + name.len();
245
246 if name_end <= search_end && name_start < content.len() && name_end <= content.len()
247 {
248 return span_to_range_with_table(content, line_table, name_start, name_end);
249 }
250 }
251 }
252 } else {
253 if let Some(pos) = content.find(name) {
255 let name_start = pos;
256 let name_end = pos + name.len();
257 if name_end <= content.len() {
258 return span_to_range_with_table(content, line_table, name_start, name_end);
259 }
260 }
261 }
262
263 Range::default()
264}
265
266fn parse_inline_table_dependency(
268 dep: &mut ParsedDependency,
269 table: &toml_edit::InlineTable,
270 content: &str,
271 line_table: &LineOffsetTable,
272) -> Result<()> {
273 for (key, value) in table {
274 match key {
275 "version" => {
276 if let Some(s) = value.as_str() {
277 dep.version_req = Some(s.to_string());
278 if let Some(span) = value.span() {
279 dep.version_range = Some(span_to_range_with_table(
280 content, line_table, span.start, span.end,
281 ));
282 }
283 }
284 }
285 "features" => {
286 if let Some(arr) = value.as_array() {
287 dep.features = arr
288 .iter()
289 .filter_map(|v| v.as_str().map(String::from))
290 .collect();
291 if let Some(span) = value.span() {
292 dep.features_range = Some(span_to_range_with_table(
293 content, line_table, span.start, span.end,
294 ));
295 }
296 }
297 }
298 "workspace" if value.as_bool() == Some(true) => {
299 dep.workspace_inherited = true;
300 }
301 "git" => {
302 if let Some(url) = value.as_str() {
303 dep.source = DependencySource::Git {
304 url: url.to_string(),
305 rev: None,
306 };
307 }
308 }
309 "path" => {
310 if let Some(path) = value.as_str() {
311 dep.source = DependencySource::Path {
312 path: path.to_string(),
313 };
314 }
315 }
316 _ => {}
317 }
318 }
319
320 Ok(())
321}
322
323fn parse_table_dependency(
325 dep: &mut ParsedDependency,
326 table: &Table,
327 content: &str,
328 line_table: &LineOffsetTable,
329) -> Result<()> {
330 for (key, item) in table {
331 let Item::Value(value) = item else {
332 continue;
333 };
334
335 match key {
336 "version" => {
337 if let Some(s) = value.as_str() {
338 dep.version_req = Some(s.to_string());
339 if let Some(span) = value.span() {
340 dep.version_range = Some(span_to_range_with_table(
341 content, line_table, span.start, span.end,
342 ));
343 }
344 }
345 }
346 "features" => {
347 if let Some(arr) = value.as_array() {
348 dep.features = arr
349 .iter()
350 .filter_map(|v| v.as_str().map(String::from))
351 .collect();
352 if let Some(span) = value.span() {
353 dep.features_range = Some(span_to_range_with_table(
354 content, line_table, span.start, span.end,
355 ));
356 }
357 }
358 }
359 "workspace" if value.as_bool() == Some(true) => {
360 dep.workspace_inherited = true;
361 }
362 "git" => {
363 if let Some(url) = value.as_str() {
364 dep.source = DependencySource::Git {
365 url: url.to_string(),
366 rev: None,
367 };
368 }
369 }
370 "path" => {
371 if let Some(path) = value.as_str() {
372 dep.source = DependencySource::Path {
373 path: path.to_string(),
374 };
375 }
376 }
377 _ => {}
378 }
379 }
380
381 Ok(())
382}
383
384fn span_to_range_with_table(
386 content: &str,
387 line_table: &LineOffsetTable,
388 start: usize,
389 end: usize,
390) -> Range {
391 let start_pos = line_table.byte_offset_to_position(content, start);
392 let end_pos = line_table.byte_offset_to_position(content, end);
393 Range::new(start_pos, end_pos)
394}
395
396fn find_workspace_root(doc_uri: &Uri) -> Result<Option<PathBuf>> {
400 let path = doc_uri
401 .to_file_path()
402 .ok_or_else(|| CargoError::invalid_uri(format!("{doc_uri:?}")))?;
403
404 let mut current = path.parent();
405
406 while let Some(dir) = current {
407 let workspace_toml = dir.join("Cargo.toml");
408
409 if workspace_toml.exists()
410 && let Ok(content) = std::fs::read_to_string(&workspace_toml)
411 && let Ok(doc) = content.parse::<DocumentMut>()
412 && doc.get("workspace").is_some()
413 {
414 return Ok(Some(dir.to_path_buf()));
415 }
416
417 current = dir.parent();
418 }
419
420 Ok(None)
421}
422
423pub struct CargoParser;
425
426impl deps_core::ManifestParser for CargoParser {
427 type Dependency = ParsedDependency;
428 type ParseResult = ParseResult;
429
430 fn parse(&self, content: &str, doc_uri: &Uri) -> deps_core::Result<Self::ParseResult> {
431 parse_cargo_toml(content, doc_uri).map_err(Into::into)
432 }
433}
434
435impl deps_core::DependencyInfo for ParsedDependency {
437 fn name(&self) -> &str {
438 &self.name
439 }
440
441 fn name_range(&self) -> Range {
442 self.name_range
443 }
444
445 fn version_requirement(&self) -> Option<&str> {
446 self.version_req.as_deref()
447 }
448
449 fn version_range(&self) -> Option<Range> {
450 self.version_range
451 }
452
453 fn source(&self) -> deps_core::DependencySource {
454 match &self.source {
455 DependencySource::Registry => deps_core::DependencySource::Registry,
456 DependencySource::Git { url, rev } => deps_core::DependencySource::Git {
457 url: url.clone(),
458 rev: rev.clone(),
459 },
460 DependencySource::Path { path } => {
461 deps_core::DependencySource::Path { path: path.clone() }
462 }
463 }
464 }
465
466 fn features(&self) -> &[String] {
467 &self.features
468 }
469}
470
471impl deps_core::ParseResultInfo for ParseResult {
473 type Dependency = ParsedDependency;
474
475 fn dependencies(&self) -> &[Self::Dependency] {
476 &self.dependencies
477 }
478
479 fn workspace_root(&self) -> Option<&std::path::Path> {
480 self.workspace_root.as_deref()
481 }
482}
483
484impl deps_core::ParseResult for ParseResult {
486 fn dependencies(&self) -> Vec<&dyn deps_core::Dependency> {
487 self.dependencies
488 .iter()
489 .map(|d| d as &dyn deps_core::Dependency)
490 .collect()
491 }
492
493 fn workspace_root(&self) -> Option<&std::path::Path> {
494 self.workspace_root.as_deref()
495 }
496
497 fn uri(&self) -> &Uri {
498 &self.uri
499 }
500
501 fn as_any(&self) -> &dyn Any {
502 self
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 fn test_url() -> Uri {
511 #[cfg(windows)]
512 let path = "C:/test/Cargo.toml";
513 #[cfg(not(windows))]
514 let path = "/test/Cargo.toml";
515 Uri::from_file_path(path).unwrap()
516 }
517
518 #[test]
519 fn test_parse_inline_dependency() {
520 let toml = r#"[dependencies]
521serde = "1.0""#;
522 let result = parse_cargo_toml(toml, &test_url()).unwrap();
523 assert_eq!(result.dependencies.len(), 1);
524 assert_eq!(result.dependencies[0].name, "serde");
525 assert_eq!(result.dependencies[0].version_req, Some("1.0".into()));
526 assert!(matches!(
527 result.dependencies[0].source,
528 DependencySource::Registry
529 ));
530 }
531
532 #[test]
533 fn test_parse_table_dependency() {
534 let toml = r#"[dependencies]
535serde = { version = "1.0", features = ["derive"] }"#;
536 let result = parse_cargo_toml(toml, &test_url()).unwrap();
537 assert_eq!(result.dependencies.len(), 1);
538 assert_eq!(result.dependencies[0].version_req, Some("1.0".into()));
539 assert_eq!(result.dependencies[0].features, vec!["derive"]);
540 }
541
542 #[test]
543 fn test_parse_workspace_inheritance() {
544 let toml = r"[dependencies]
545serde = { workspace = true }";
546 let result = parse_cargo_toml(toml, &test_url()).unwrap();
547 assert_eq!(result.dependencies.len(), 1);
548 assert!(result.dependencies[0].workspace_inherited);
549 }
550
551 #[test]
552 fn test_parse_git_dependency() {
553 let toml = r#"[dependencies]
554tower-lsp = { git = "https://github.com/ebkalderon/tower-lsp", branch = "main" }"#;
555 let result = parse_cargo_toml(toml, &test_url()).unwrap();
556 assert_eq!(result.dependencies.len(), 1);
557 assert!(matches!(
558 result.dependencies[0].source,
559 DependencySource::Git { .. }
560 ));
561 }
562
563 #[test]
564 fn test_parse_path_dependency() {
565 let toml = r#"[dependencies]
566local = { path = "../local" }"#;
567 let result = parse_cargo_toml(toml, &test_url()).unwrap();
568 assert_eq!(result.dependencies.len(), 1);
569 assert!(matches!(
570 result.dependencies[0].source,
571 DependencySource::Path { .. }
572 ));
573 }
574
575 #[test]
576 fn test_parse_multiple_sections() {
577 let toml = r#"
578[dependencies]
579serde = "1.0"
580
581[dev-dependencies]
582insta = "1.0"
583
584[build-dependencies]
585cc = "1.0"
586"#;
587 let result = parse_cargo_toml(toml, &test_url()).unwrap();
588 assert_eq!(result.dependencies.len(), 3);
589
590 assert!(matches!(
591 result.dependencies[0].section,
592 DependencySection::Dependencies
593 ));
594 assert!(matches!(
595 result.dependencies[1].section,
596 DependencySection::DevDependencies
597 ));
598 assert!(matches!(
599 result.dependencies[2].section,
600 DependencySection::BuildDependencies
601 ));
602 }
603
604 #[test]
605 fn test_line_offset_table() {
606 let content = "abc\ndef";
607 let table = LineOffsetTable::new(content);
608 let pos = table.byte_offset_to_position(content, 4);
609 assert_eq!(pos.line, 1);
610 assert_eq!(pos.character, 0);
611 }
612
613 #[test]
614 fn test_line_offset_table_unicode() {
615 let content = "hello 世界\nworld";
616 let table = LineOffsetTable::new(content);
617 let world_offset = content.find("world").unwrap();
618 let pos = table.byte_offset_to_position(content, world_offset);
619 assert_eq!(pos.line, 1);
620 assert_eq!(pos.character, 0);
621 }
622
623 #[test]
624 fn test_malformed_toml() {
625 let toml = r#"[dependencies
626serde = "1.0"#;
627 let result = parse_cargo_toml(toml, &test_url());
628 assert!(result.is_err());
629 }
630
631 #[test]
632 fn test_empty_dependencies() {
633 let toml = r"[dependencies]";
634 let result = parse_cargo_toml(toml, &test_url()).unwrap();
635 assert_eq!(result.dependencies.len(), 0);
636 }
637
638 #[test]
639 fn test_position_tracking() {
640 let toml = r#"[dependencies]
641serde = "1.0""#;
642 let result = parse_cargo_toml(toml, &test_url()).unwrap();
643 let dep = &result.dependencies[0];
644
645 assert_eq!(dep.name, "serde");
646 assert_eq!(dep.version_req, Some("1.0".into()));
647
648 assert_eq!(dep.name_range.start.line, 1);
650 assert_eq!(dep.name_range.start.character, 0);
652 assert_eq!(dep.name_range.end.character, 5);
654 }
655
656 #[test]
657 fn test_name_range_tracking() {
658 let toml = r#"[dependencies]
659serde = "1.0"
660tokio = { version = "1.0", features = ["full"] }"#;
661 let result = parse_cargo_toml(toml, &test_url()).unwrap();
662
663 for dep in &result.dependencies {
664 let is_default = dep.name_range.start.line == 0
666 && dep.name_range.start.character == 0
667 && dep.name_range.end.line == 0
668 && dep.name_range.end.character == 0;
669 assert!(
670 !is_default,
671 "name_range should not be default for {}",
672 dep.name
673 );
674 }
675 }
676
677 #[test]
678 fn test_parse_workspace_dependencies() {
679 let toml = r#"
680[workspace]
681members = ["crates/*"]
682
683[workspace.dependencies]
684serde = "1.0"
685tokio = { version = "1.0", features = ["full"] }
686"#;
687 let result = parse_cargo_toml(toml, &test_url()).unwrap();
688 assert_eq!(result.dependencies.len(), 2);
689
690 for dep in &result.dependencies {
691 assert!(matches!(
692 dep.section,
693 DependencySection::WorkspaceDependencies
694 ));
695 }
696
697 let serde = result.dependencies.iter().find(|d| d.name == "serde");
698 assert!(serde.is_some());
699 let serde = serde.unwrap();
700 assert_eq!(serde.version_req, Some("1.0".into()));
701 assert!(
703 serde.version_range.is_some(),
704 "version_range should be set for serde"
705 );
706
707 let tokio = result.dependencies.iter().find(|d| d.name == "tokio");
708 assert!(tokio.is_some());
709 let tokio = tokio.unwrap();
710 assert_eq!(tokio.version_req, Some("1.0".into()));
711 assert_eq!(tokio.features, vec!["full"]);
712 assert!(
714 tokio.version_range.is_some(),
715 "version_range should be set for tokio"
716 );
717 }
718
719 #[test]
720 fn test_parse_workspace_and_regular_dependencies() {
721 let toml = r#"
722[workspace]
723members = ["crates/*"]
724
725[workspace.dependencies]
726serde = "1.0"
727
728[dependencies]
729tokio = "1.0"
730"#;
731 let result = parse_cargo_toml(toml, &test_url()).unwrap();
732 assert_eq!(result.dependencies.len(), 2);
733
734 let serde = result.dependencies.iter().find(|d| d.name == "serde");
735 assert!(serde.is_some());
736 assert!(matches!(
737 serde.unwrap().section,
738 DependencySection::WorkspaceDependencies
739 ));
740
741 let tokio = result.dependencies.iter().find(|d| d.name == "tokio");
742 assert!(tokio.is_some());
743 assert!(matches!(
744 tokio.unwrap().section,
745 DependencySection::Dependencies
746 ));
747 }
748}