1use crate::error::{CargoError, Result};
32use crate::types::{DependencySection, DependencySource, ParsedDependency};
33use std::any::Any;
34use std::path::PathBuf;
35use toml_span::value::{Table, Value};
36use tower_lsp_server::ls_types::{Range, Uri};
37
38pub use deps_core::lsp_helpers::LineOffsetTable;
39
40#[derive(Debug, Clone)]
45pub struct ParseResult {
46 pub dependencies: Vec<ParsedDependency>,
48 pub workspace_root: Option<PathBuf>,
50 pub uri: Uri,
52}
53
54pub fn parse_cargo_toml(content: &str, doc_uri: &Uri) -> Result<ParseResult> {
79 let doc = toml_span::parse(content).map_err(|e| CargoError::TomlParseError {
80 message: e.to_string(),
81 })?;
82
83 let line_table = LineOffsetTable::new(content);
84 let mut dependencies = Vec::new();
85
86 let root_table = doc.as_table().ok_or_else(|| CargoError::TomlParseError {
87 message: "root is not a table".into(),
88 })?;
89
90 if let Some(deps_val) = get_val(root_table, "dependencies")
91 && let Some(deps) = deps_val.as_table()
92 {
93 dependencies.extend(parse_dependencies_section(
94 deps,
95 content,
96 &line_table,
97 DependencySection::Dependencies,
98 ));
99 }
100
101 if let Some(dev_deps_val) = get_val(root_table, "dev-dependencies")
102 && let Some(dev_deps) = dev_deps_val.as_table()
103 {
104 dependencies.extend(parse_dependencies_section(
105 dev_deps,
106 content,
107 &line_table,
108 DependencySection::DevDependencies,
109 ));
110 }
111
112 if let Some(build_deps_val) = get_val(root_table, "build-dependencies")
113 && let Some(build_deps) = build_deps_val.as_table()
114 {
115 dependencies.extend(parse_dependencies_section(
116 build_deps,
117 content,
118 &line_table,
119 DependencySection::BuildDependencies,
120 ));
121 }
122
123 if let Some(workspace_val) = get_val(root_table, "workspace")
125 && let Some(workspace_table) = workspace_val.as_table()
126 && let Some(workspace_deps_val) = get_val(workspace_table, "dependencies")
127 && let Some(workspace_deps) = workspace_deps_val.as_table()
128 {
129 dependencies.extend(parse_dependencies_section(
130 workspace_deps,
131 content,
132 &line_table,
133 DependencySection::WorkspaceDependencies,
134 ));
135 }
136
137 let workspace_root = find_workspace_root(doc_uri)?;
138
139 Ok(ParseResult {
140 dependencies,
141 workspace_root,
142 uri: doc_uri.clone(),
143 })
144}
145
146fn get_val<'a>(table: &'a Table<'a>, key: &str) -> Option<&'a Value<'a>> {
147 table.get(key)
148}
149
150fn parse_dependencies_section(
152 table: &Table<'_>,
153 content: &str,
154 line_table: &LineOffsetTable,
155 section: DependencySection,
156) -> Vec<ParsedDependency> {
157 let mut deps = Vec::new();
158
159 for (key, value) in table {
160 let name = key.name.to_string();
161 let name_range = span_to_range(content, line_table, key.span);
162
163 let mut dep = ParsedDependency {
164 name,
165 name_range,
166 version_req: None,
167 version_range: None,
168 features: Vec::new(),
169 features_range: None,
170 source: DependencySource::Registry,
171 section,
172 };
173
174 if let Some(s) = value.as_str() {
175 dep.version_req = Some(s.to_string());
177 dep.version_range = Some(span_to_range(content, line_table, value.span));
178 } else if let Some(t) = value.as_table() {
179 parse_table_dependency(&mut dep, t, content, line_table);
181 } else {
182 continue;
183 }
184
185 deps.push(dep);
186 }
187
188 deps
189}
190
191fn parse_table_dependency(
193 dep: &mut ParsedDependency,
194 table: &Table<'_>,
195 content: &str,
196 line_table: &LineOffsetTable,
197) {
198 for (key, value) in table {
199 match key.name.as_ref() {
200 "version" => {
201 if let Some(s) = value.as_str() {
202 dep.version_req = Some(s.to_string());
203 dep.version_range = Some(span_to_range(content, line_table, value.span));
204 }
205 }
206 "features" => {
207 if let Some(arr) = value.as_array() {
208 dep.features = arr
209 .iter()
210 .filter_map(|v| v.as_str().map(String::from))
211 .collect();
212 dep.features_range = Some(span_to_range(content, line_table, value.span));
213 }
214 }
215 "workspace" if value.as_bool() == Some(true) => {
216 dep.source = DependencySource::Workspace;
217 }
218 "workspace" => {}
219 "git" => {
220 if let Some(url) = value.as_str() {
221 dep.source = DependencySource::Git {
222 url: url.to_string(),
223 rev: None,
224 };
225 }
226 }
227 "path" => {
228 if let Some(path) = value.as_str() {
229 dep.source = DependencySource::Path {
230 path: path.to_string(),
231 };
232 }
233 }
234 _ => {}
235 }
236 }
237}
238
239fn span_to_range(content: &str, line_table: &LineOffsetTable, span: toml_span::Span) -> Range {
241 let start = line_table.byte_offset_to_position(content, span.start);
242 let end = line_table.byte_offset_to_position(content, span.end);
243 Range::new(start, end)
244}
245
246fn find_workspace_root(doc_uri: &Uri) -> Result<Option<PathBuf>> {
250 let path = doc_uri
251 .to_file_path()
252 .ok_or_else(|| CargoError::invalid_uri(format!("{doc_uri:?}")))?;
253
254 let mut current = path.parent();
255
256 while let Some(dir) = current {
257 let workspace_toml = dir.join("Cargo.toml");
258
259 if workspace_toml.exists()
260 && let Ok(content) = std::fs::read_to_string(&workspace_toml)
261 && let Ok(doc) = toml_span::parse(&content)
262 && doc
263 .as_table()
264 .and_then(|t| get_val(t, "workspace"))
265 .is_some()
266 {
267 return Ok(Some(dir.to_path_buf()));
268 }
269
270 current = dir.parent();
271 }
272
273 Ok(None)
274}
275
276pub struct CargoParser;
278
279impl deps_core::ManifestParser for CargoParser {
280 type Dependency = ParsedDependency;
281 type ParseResult = ParseResult;
282
283 fn parse(&self, content: &str, doc_uri: &Uri) -> deps_core::Result<Self::ParseResult> {
284 parse_cargo_toml(content, doc_uri).map_err(Into::into)
285 }
286}
287
288impl deps_core::DependencyInfo for ParsedDependency {
290 fn name(&self) -> &str {
291 &self.name
292 }
293
294 fn name_range(&self) -> Range {
295 self.name_range
296 }
297
298 fn version_requirement(&self) -> Option<&str> {
299 self.version_req.as_deref()
300 }
301
302 fn version_range(&self) -> Option<Range> {
303 self.version_range
304 }
305
306 fn source(&self) -> deps_core::DependencySource {
307 self.source.clone()
308 }
309
310 fn features(&self) -> &[String] {
311 &self.features
312 }
313}
314
315impl deps_core::ParseResultInfo for ParseResult {
317 type Dependency = ParsedDependency;
318
319 fn dependencies(&self) -> &[Self::Dependency] {
320 &self.dependencies
321 }
322
323 fn workspace_root(&self) -> Option<&std::path::Path> {
324 self.workspace_root.as_deref()
325 }
326}
327
328impl deps_core::ParseResult for ParseResult {
330 fn dependencies(&self) -> Vec<&dyn deps_core::Dependency> {
331 self.dependencies
332 .iter()
333 .map(|d| d as &dyn deps_core::Dependency)
334 .collect()
335 }
336
337 fn workspace_root(&self) -> Option<&std::path::Path> {
338 self.workspace_root.as_deref()
339 }
340
341 fn uri(&self) -> &Uri {
342 &self.uri
343 }
344
345 fn as_any(&self) -> &dyn Any {
346 self
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 fn test_url() -> Uri {
355 #[cfg(windows)]
356 let path = "C:/test/Cargo.toml";
357 #[cfg(not(windows))]
358 let path = "/test/Cargo.toml";
359 Uri::from_file_path(path).unwrap()
360 }
361
362 #[test]
363 fn test_parse_inline_dependency() {
364 let toml = r#"[dependencies]
365serde = "1.0""#;
366 let result = parse_cargo_toml(toml, &test_url()).unwrap();
367 assert_eq!(result.dependencies.len(), 1);
368 assert_eq!(result.dependencies[0].name, "serde");
369 assert_eq!(result.dependencies[0].version_req, Some("1.0".into()));
370 assert!(matches!(
371 result.dependencies[0].source,
372 DependencySource::Registry
373 ));
374 }
375
376 #[test]
377 fn test_parse_table_dependency() {
378 let toml = r#"[dependencies]
379serde = { version = "1.0", features = ["derive"] }"#;
380 let result = parse_cargo_toml(toml, &test_url()).unwrap();
381 assert_eq!(result.dependencies.len(), 1);
382 assert_eq!(result.dependencies[0].version_req, Some("1.0".into()));
383 assert_eq!(result.dependencies[0].features, vec!["derive"]);
384 }
385
386 #[test]
387 fn test_parse_workspace_inheritance() {
388 let toml = r"[dependencies]
389serde = { workspace = true }";
390 let result = parse_cargo_toml(toml, &test_url()).unwrap();
391 assert_eq!(result.dependencies.len(), 1);
392 assert!(matches!(
393 result.dependencies[0].source,
394 DependencySource::Workspace
395 ));
396 }
397
398 #[test]
399 fn test_parse_git_dependency() {
400 let toml = r#"[dependencies]
401tower-lsp = { git = "https://github.com/ebkalderon/tower-lsp", branch = "main" }"#;
402 let result = parse_cargo_toml(toml, &test_url()).unwrap();
403 assert_eq!(result.dependencies.len(), 1);
404 assert!(matches!(
405 result.dependencies[0].source,
406 DependencySource::Git { .. }
407 ));
408 }
409
410 #[test]
411 fn test_parse_path_dependency() {
412 let toml = r#"[dependencies]
413local = { path = "../local" }"#;
414 let result = parse_cargo_toml(toml, &test_url()).unwrap();
415 assert_eq!(result.dependencies.len(), 1);
416 assert!(matches!(
417 result.dependencies[0].source,
418 DependencySource::Path { .. }
419 ));
420 }
421
422 #[test]
423 fn test_parse_multiple_sections() {
424 let toml = r#"
425[dependencies]
426serde = "1.0"
427
428[dev-dependencies]
429insta = "1.0"
430
431[build-dependencies]
432cc = "1.0"
433"#;
434 let result = parse_cargo_toml(toml, &test_url()).unwrap();
435 assert_eq!(result.dependencies.len(), 3);
436
437 assert!(matches!(
438 result.dependencies[0].section,
439 DependencySection::Dependencies
440 ));
441 assert!(matches!(
442 result.dependencies[1].section,
443 DependencySection::DevDependencies
444 ));
445 assert!(matches!(
446 result.dependencies[2].section,
447 DependencySection::BuildDependencies
448 ));
449 }
450
451 #[test]
452 fn test_line_offset_table() {
453 let content = "abc\ndef";
454 let table = LineOffsetTable::new(content);
455 let pos = table.byte_offset_to_position(content, 4);
456 assert_eq!(pos.line, 1);
457 assert_eq!(pos.character, 0);
458 }
459
460 #[test]
461 fn test_line_offset_table_unicode() {
462 let content = "hello 世界\nworld";
463 let table = LineOffsetTable::new(content);
464 let world_offset = content.find("world").unwrap();
465 let pos = table.byte_offset_to_position(content, world_offset);
466 assert_eq!(pos.line, 1);
467 assert_eq!(pos.character, 0);
468 }
469
470 #[test]
471 fn test_malformed_toml() {
472 let toml = r#"[dependencies
473serde = "1.0"#;
474 let result = parse_cargo_toml(toml, &test_url());
475 assert!(result.is_err());
476 }
477
478 #[test]
479 fn test_empty_dependencies() {
480 let toml = r"[dependencies]";
481 let result = parse_cargo_toml(toml, &test_url()).unwrap();
482 assert_eq!(result.dependencies.len(), 0);
483 }
484
485 #[test]
486 fn test_position_tracking() {
487 let toml = r#"[dependencies]
488serde = "1.0""#;
489 let result = parse_cargo_toml(toml, &test_url()).unwrap();
490 let dep = &result.dependencies[0];
491
492 assert_eq!(dep.name, "serde");
493 assert_eq!(dep.version_req, Some("1.0".into()));
494
495 assert_eq!(dep.name_range.start.line, 1);
497 assert_eq!(dep.name_range.start.character, 0);
499 assert_eq!(dep.name_range.end.character, 5);
501 }
502
503 #[test]
504 fn test_name_range_tracking() {
505 let toml = r#"[dependencies]
506serde = "1.0"
507tokio = { version = "1.0", features = ["full"] }"#;
508 let result = parse_cargo_toml(toml, &test_url()).unwrap();
509
510 for dep in &result.dependencies {
511 let is_default = dep.name_range.start.line == 0
513 && dep.name_range.start.character == 0
514 && dep.name_range.end.line == 0
515 && dep.name_range.end.character == 0;
516 assert!(
517 !is_default,
518 "name_range should not be default for {}",
519 dep.name
520 );
521 }
522 }
523
524 #[test]
525 fn test_parse_workspace_dependencies() {
526 let toml = r#"
527[workspace]
528members = ["crates/*"]
529
530[workspace.dependencies]
531serde = "1.0"
532tokio = { version = "1.0", features = ["full"] }
533"#;
534 let result = parse_cargo_toml(toml, &test_url()).unwrap();
535 assert_eq!(result.dependencies.len(), 2);
536
537 for dep in &result.dependencies {
538 assert!(matches!(
539 dep.section,
540 DependencySection::WorkspaceDependencies
541 ));
542 }
543
544 let serde = result.dependencies.iter().find(|d| d.name == "serde");
545 assert!(serde.is_some());
546 let serde = serde.unwrap();
547 assert_eq!(serde.version_req, Some("1.0".into()));
548 assert!(
550 serde.version_range.is_some(),
551 "version_range should be set for serde"
552 );
553
554 let tokio = result.dependencies.iter().find(|d| d.name == "tokio");
555 assert!(tokio.is_some());
556 let tokio = tokio.unwrap();
557 assert_eq!(tokio.version_req, Some("1.0".into()));
558 assert_eq!(tokio.features, vec!["full"]);
559 assert!(
561 tokio.version_range.is_some(),
562 "version_range should be set for tokio"
563 );
564 }
565
566 #[test]
567 fn test_parse_workspace_and_regular_dependencies() {
568 let toml = r#"
569[workspace]
570members = ["crates/*"]
571
572[workspace.dependencies]
573serde = "1.0"
574
575[dependencies]
576tokio = "1.0"
577"#;
578 let result = parse_cargo_toml(toml, &test_url()).unwrap();
579 assert_eq!(result.dependencies.len(), 2);
580
581 let serde = result.dependencies.iter().find(|d| d.name == "serde");
582 assert!(serde.is_some());
583 assert!(matches!(
584 serde.unwrap().section,
585 DependencySection::WorkspaceDependencies
586 ));
587
588 let tokio = result.dependencies.iter().find(|d| d.name == "tokio");
589 assert!(tokio.is_some());
590 assert!(matches!(
591 tokio.unwrap().section,
592 DependencySection::Dependencies
593 ));
594 }
595}