Skip to content

Commit cc947a3

Browse files
committed
feat(swift): add Swift Package Manager ecosystem support
Implement deps-swift crate with Package.swift parsing (regex-based, 9 .package() patterns), GitHub API registry for version resolution, Package.resolved lockfile support (v1/v2/v3), and ecosystem integration.
1 parent 5aae3b2 commit cc947a3

File tree

16 files changed

+2454
-1
lines changed

16 files changed

+2454
-1
lines changed

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ deps-bundler = { version = "0.8.0", path = "crates/deps-bundler" }
2424
deps-dart = { version = "0.8.0", path = "crates/deps-dart" }
2525
deps-maven = { version = "0.8.0", path = "crates/deps-maven" }
2626
deps-gradle = { version = "0.8.0", path = "crates/deps-gradle" }
27+
deps-swift = { version = "0.8.0", path = "crates/deps-swift" }
2728
deps-lsp = { version = "0.8.0", path = "crates/deps-lsp" }
2829
futures = "0.3"
2930
insta = "1"

crates/deps-lsp/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ name = "deps_lsp"
2121
path = "src/lib.rs"
2222

2323
[features]
24-
default = ["cargo", "npm", "pypi", "go", "bundler", "dart", "maven", "gradle"]
24+
default = ["cargo", "npm", "pypi", "go", "bundler", "dart", "maven", "gradle", "swift"]
2525
cargo = ["dep:deps-cargo"]
2626
npm = ["dep:deps-npm"]
2727
pypi = ["dep:deps-pypi"]
@@ -30,6 +30,7 @@ bundler = ["dep:deps-bundler"]
3030
dart = ["dep:deps-dart"]
3131
maven = ["dep:deps-maven"]
3232
gradle = ["dep:deps-gradle"]
33+
swift = ["dep:deps-swift"]
3334

3435
[dependencies]
3536
# Internal crates
@@ -42,6 +43,7 @@ deps-bundler = { workspace = true, optional = true }
4243
deps-dart = { workspace = true, optional = true }
4344
deps-maven = { workspace = true, optional = true }
4445
deps-gradle = { workspace = true, optional = true }
46+
deps-swift = { workspace = true, optional = true }
4547

4648
# External dependencies
4749
dashmap = { workspace = true }

crates/deps-lsp/src/lib.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,22 @@ ecosystem!(
152152
]
153153
);
154154

155+
ecosystem!(
156+
"swift",
157+
deps_swift,
158+
SwiftEcosystem,
159+
[
160+
SwiftDependency,
161+
SwiftParseResult,
162+
SwiftVersion,
163+
SwiftPackage,
164+
SwiftFormatter,
165+
SwiftRegistry,
166+
SwiftLockParser,
167+
parse_package_swift,
168+
]
169+
);
170+
155171
/// Registers all enabled ecosystems.
156172
pub fn register_ecosystems(registry: &EcosystemRegistry, cache: Arc<HttpCache>) {
157173
register!("cargo", CargoEcosystem, registry, &cache);
@@ -162,6 +178,7 @@ pub fn register_ecosystems(registry: &EcosystemRegistry, cache: Arc<HttpCache>)
162178
register!("dart", DartEcosystem, registry, &cache);
163179
register!("maven", MavenEcosystem, registry, &cache);
164180
register!("gradle", GradleEcosystem, registry, &cache);
181+
register!("swift", SwiftEcosystem, registry, &cache);
165182
}
166183

167184
#[cfg(test)]

crates/deps-swift/Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
name = "deps-swift"
3+
version.workspace = true
4+
edition.workspace = true
5+
rust-version.workspace = true
6+
authors.workspace = true
7+
license.workspace = true
8+
repository.workspace = true
9+
description = "Swift Package Manager support for deps-lsp"
10+
11+
[lints]
12+
workspace = true
13+
14+
[dependencies]
15+
deps-core = { workspace = true }
16+
regex = { workspace = true }
17+
semver = { workspace = true }
18+
serde = { workspace = true, features = ["derive"] }
19+
serde_json = { workspace = true }
20+
tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread"] }
21+
tower-lsp-server = { workspace = true }
22+
tracing = { workspace = true }
23+
thiserror = { workspace = true }
24+
urlencoding = { workspace = true }
25+
26+
[dev-dependencies]
27+
insta = { workspace = true, features = ["json"] }
28+
tempfile = { workspace = true }
29+
tokio-test = { workspace = true }

crates/deps-swift/src/ecosystem.rs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
//! Swift ecosystem implementation for deps-lsp.
2+
3+
use std::any::Any;
4+
use std::sync::Arc;
5+
use tower_lsp_server::ls_types::{CompletionItem, Position, Uri};
6+
7+
use deps_core::{
8+
Ecosystem, ParseResult as ParseResultTrait, Registry, Result, lsp_helpers::EcosystemFormatter,
9+
};
10+
11+
use crate::formatter::SwiftFormatter;
12+
use crate::lockfile::SwiftLockParser;
13+
use crate::registry::SwiftRegistry;
14+
15+
/// Swift/SPM ecosystem implementation.
16+
///
17+
/// Provides LSP functionality for Package.swift files, including:
18+
/// - Dependency parsing with position tracking
19+
/// - Version information from GitHub tags
20+
/// - Inlay hints for latest versions
21+
/// - Hover tooltips with package metadata
22+
/// - Code actions for version updates
23+
/// - Diagnostics for unknown packages
24+
pub struct SwiftEcosystem {
25+
registry: Arc<SwiftRegistry>,
26+
formatter: SwiftFormatter,
27+
lockfile_provider: Arc<SwiftLockParser>,
28+
}
29+
30+
impl SwiftEcosystem {
31+
/// Creates a new Swift ecosystem with the given HTTP cache.
32+
pub fn new(cache: Arc<deps_core::HttpCache>) -> Self {
33+
Self {
34+
registry: Arc::new(SwiftRegistry::new(cache)),
35+
formatter: SwiftFormatter,
36+
lockfile_provider: Arc::new(SwiftLockParser),
37+
}
38+
}
39+
40+
async fn complete_package_names(&self, prefix: &str) -> Vec<CompletionItem> {
41+
deps_core::completion::complete_package_names_generic(self.registry.as_ref(), prefix, 20)
42+
.await
43+
}
44+
45+
async fn complete_versions(&self, package_name: &str, prefix: &str) -> Vec<CompletionItem> {
46+
deps_core::completion::complete_versions_generic(
47+
self.registry.as_ref(),
48+
package_name,
49+
prefix,
50+
&[],
51+
)
52+
.await
53+
}
54+
}
55+
56+
impl deps_core::ecosystem::private::Sealed for SwiftEcosystem {}
57+
58+
impl Ecosystem for SwiftEcosystem {
59+
fn id(&self) -> &'static str {
60+
"swift"
61+
}
62+
63+
fn display_name(&self) -> &'static str {
64+
"Swift (SPM)"
65+
}
66+
67+
fn manifest_filenames(&self) -> &[&'static str] {
68+
&["Package.swift"]
69+
}
70+
71+
fn lockfile_filenames(&self) -> &[&'static str] {
72+
&["Package.resolved"]
73+
}
74+
75+
fn parse_manifest<'a>(
76+
&'a self,
77+
content: &'a str,
78+
uri: &'a Uri,
79+
) -> deps_core::ecosystem::BoxFuture<'a, Result<Box<dyn ParseResultTrait>>> {
80+
Box::pin(async move {
81+
let result = crate::parser::parse_package_swift(content, uri)?;
82+
Ok(Box::new(result) as Box<dyn ParseResultTrait>)
83+
})
84+
}
85+
86+
fn registry(&self) -> Arc<dyn Registry> {
87+
self.registry.clone() as Arc<dyn Registry>
88+
}
89+
90+
fn lockfile_provider(&self) -> Option<Arc<dyn deps_core::lockfile::LockFileProvider>> {
91+
Some(self.lockfile_provider.clone() as Arc<dyn deps_core::lockfile::LockFileProvider>)
92+
}
93+
94+
fn formatter(&self) -> &dyn EcosystemFormatter {
95+
&self.formatter
96+
}
97+
98+
fn generate_completions<'a>(
99+
&'a self,
100+
parse_result: &'a dyn ParseResultTrait,
101+
position: Position,
102+
content: &'a str,
103+
) -> deps_core::ecosystem::BoxFuture<'a, Vec<CompletionItem>> {
104+
Box::pin(async move {
105+
use deps_core::completion::{CompletionContext, detect_completion_context};
106+
107+
let context = detect_completion_context(parse_result, position, content);
108+
109+
match context {
110+
CompletionContext::PackageName { prefix } => {
111+
// Strip "https://github.com/" prefix for search query
112+
let query = prefix
113+
.strip_prefix("https://github.com/")
114+
.or_else(|| prefix.strip_prefix("https://github.com"))
115+
.unwrap_or(&prefix);
116+
if query.len() < 2 {
117+
return vec![];
118+
}
119+
self.complete_package_names(query).await
120+
}
121+
CompletionContext::Version {
122+
package_name,
123+
prefix,
124+
} => self.complete_versions(&package_name, &prefix).await,
125+
CompletionContext::Feature { .. } => vec![],
126+
CompletionContext::None => vec![],
127+
}
128+
})
129+
}
130+
131+
fn as_any(&self) -> &dyn Any {
132+
self
133+
}
134+
}
135+
136+
#[cfg(test)]
137+
mod tests {
138+
use super::*;
139+
140+
#[test]
141+
fn test_ecosystem_id() {
142+
let cache = Arc::new(deps_core::HttpCache::new());
143+
let eco = SwiftEcosystem::new(cache);
144+
assert_eq!(eco.id(), "swift");
145+
}
146+
147+
#[test]
148+
fn test_ecosystem_display_name() {
149+
let cache = Arc::new(deps_core::HttpCache::new());
150+
let eco = SwiftEcosystem::new(cache);
151+
assert_eq!(eco.display_name(), "Swift (SPM)");
152+
}
153+
154+
#[test]
155+
fn test_manifest_filenames() {
156+
let cache = Arc::new(deps_core::HttpCache::new());
157+
let eco = SwiftEcosystem::new(cache);
158+
assert_eq!(eco.manifest_filenames(), &["Package.swift"]);
159+
assert_eq!(eco.lockfile_filenames(), &["Package.resolved"]);
160+
}
161+
162+
#[test]
163+
fn test_as_any() {
164+
let cache = Arc::new(deps_core::HttpCache::new());
165+
let eco = SwiftEcosystem::new(cache);
166+
assert!(eco.as_any().is::<SwiftEcosystem>());
167+
}
168+
169+
#[test]
170+
fn test_lockfile_provider_some() {
171+
let cache = Arc::new(deps_core::HttpCache::new());
172+
let eco = SwiftEcosystem::new(cache);
173+
assert!(eco.lockfile_provider().is_some());
174+
}
175+
176+
#[tokio::test]
177+
async fn test_parse_manifest_valid() {
178+
let cache = Arc::new(deps_core::HttpCache::new());
179+
let eco = SwiftEcosystem::new(cache);
180+
let uri = Uri::from_file_path("/test/Package.swift").unwrap();
181+
let content = r#".package(url: "https://github.com/apple/swift-nio.git", from: "2.40.0")"#;
182+
let result = eco.parse_manifest(content, &uri).await;
183+
assert!(result.is_ok());
184+
assert_eq!(result.unwrap().dependencies().len(), 1);
185+
}
186+
187+
#[tokio::test]
188+
async fn test_parse_manifest_empty() {
189+
let cache = Arc::new(deps_core::HttpCache::new());
190+
let eco = SwiftEcosystem::new(cache);
191+
let uri = Uri::from_file_path("/test/Package.swift").unwrap();
192+
let result = eco.parse_manifest("// empty file", &uri).await;
193+
assert!(result.is_ok());
194+
assert!(result.unwrap().dependencies().is_empty());
195+
}
196+
}

0 commit comments

Comments
 (0)